Skip to content

Instantly share code, notes, and snippets.

@BennieCopeland
Last active February 26, 2024 07:36
Show Gist options
  • Save BennieCopeland/e00365feaf91d4f7c468683ba5725336 to your computer and use it in GitHub Desktop.
Save BennieCopeland/e00365feaf91d4f7c468683ba5725336 to your computer and use it in GitHub Desktop.
C# Discriminated Union pattern
public record EmailAddress
{
public const int MaxSize = 255;
private readonly string _value;
private EmailAddress(string value) => _value = value;
public static EmailAddress FromString(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Email address cannot be empty");
}
if (value.Length > MaxSize)
{
throw new ArgumentException($"Email address cannot be longer than {MaxSize} characters");
}
return new EmailAddress(value);
}
public string AsString()
{
return _value;
}
}
public record TelephoneNumber
{
public const int MaxSize = 50;
private readonly string _value;
private TelephoneNumber(string value) => _value = value;
public static Result<TelephoneNumber, string> FromString(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Telephone number cannot be empty");
}
if (value.Length > MaxSize)
{
throw new ArgumentException($"Telephone number cannot be longer than {MaxSize} characters");
}
return new TelephoneNumber(value);
}
public string AsString()
{
return _value;
}
}
public abstract record ContactMethod
{
public static ContactMethod Email(EmailAddress emailAddress) => new EmailMethod(emailAddress);
public static ContactMethod Telephone(TelephoneNumber telephoneNumber) => new PhoneMethod(telephoneNumber);
public static ContactMethod CarrierPigeon(string pigeonName) => new CarrierPigeonMethod(pigeonName);
public abstract T Match<T>(Func<EmailAddress, T> email, Func<TelephoneNumber, T> phone, Func<string, T> carrierPigeon);
private sealed record EmailMethod : ContactMethod
{
private readonly EmailAddress _emailAddress;
public EmailMethod(EmailAddress emailAddress) => _emailAddress = emailAddress;
public override T Match<T>(Func<EmailAddress, T> email, Func<TelephoneNumber, T> phone, Func<string, T> carrierPigeon)
{
return email(_emailAddress);
}
}
private sealed record PhoneMethod : ContactMethod
{
private readonly TelephoneNumber _telephoneNumber;
public PhoneMethod(TelephoneNumber telephoneNumber) => _telephoneNumber = telephoneNumber;
public override T Match<T>(Func<EmailAddress, T> email, Func<TelephoneNumber, T> phone, Func<string, T> carrierPigeon)
{
return phone(_telephoneNumber);
}
}
private sealed record CarrierPigeonMethod : ContactMethod
{
private readonly string _pigeonName;
public CarrierPigeonMethod(string pigeonName) => _pigeonName = pigeonName;
public override T Match<T>(Func<EmailAddress, T> email, Func<TelephoneNumber, T> phone, Func<string, T> carrierPigeon)
{
return carrierPigeon(_pigeonName);
}
}
}
class User
{
public ContactMethod? ContactMethod { get; private set; }
public void AddEmailContact(EmailAddress emailAddress) => ContactMethod = ContactMethod.Email(emailAddress);
public void AddPhoneContact(TelephoneNumber telephoneNumber) => ContactMethod = ContactMethod.Telephone(telephoneNumber);
public void AddCarrierPigeon(string pigeonName) => ContactMethod = ContactMethod.CarrierPigeon(pigeonName);
}
interface IServices
{
void SendEmail(EmailAddress emailAddress);
void CallPhone(TelephoneNumber telephoneNumber);
void SendPigeon(string pigeonName);
}
class UserService
{
private readonly IServices _services;
public UserService(IServices services) => _services = services;
public void ContactUser(User user)
{
if (user.ContactMethod == null) throw new Exception("Can't contact user");
user.ContactMethod.Match(
emailAddress => _services.SendEmail(emailAddress),
telephoneNumber => _services.CallPhone(telephoneNumber),
pigeonName => _services.SendPigeon(pigeonName));
}
}
type EmailAddress = EmailAddress of string
with
static member MaxValue = 150
static member FromString str =
if System.String.IsNullOrWhiteSpace str then
Error "Email address cannot be empty"
elif str.Length > EmailAddress.MaxValue then
Error $"Email address cannot be longer that {EmailAddress.MaxValue} characters"
else
Ok (EmailAddress str)
static member AsString (EmailAddress emailAddress) = emailAddress
type TelephoneNumber = TelephoneNumber of string
with
static member MaxValue = 50
static member FromString str =
if System.String.IsNullOrWhiteSpace str then
Error "Telephone number cannot be empty"
elif str.Length > TelephoneNumber.MaxValue then
Error $"Telephone number cannot be longer that {TelephoneNumber.MaxValue} characters"
else
Ok (TelephoneNumber str)
static member AsString (TelephoneNumber telephoneNumber) = telephoneNumber
type ContactMethod =
| Email of EmailAddress
| Telephone of TelephoneNumber
| CarrierPigeon of string
let isEmail = function
| Email _ -> true
| _ -> false
let isTelephone = function
| Telephone _ -> true
| _ -> false
let isCarrierPigeon = function
| CarrierPigeon _ -> true
| _ -> false
type User =
{
ContactMethod : ContactMethod option
}
let addEmailContact user emailAddress =
{ user with ContactMethod = Some (Email emailAddress) }
let addPhoneContact user phoneNumber =
{ user with ContactMethod = Some (Telephone phoneNumber) }
let addCarrierPigeon user pigeonName =
{ user with ContactMethod = Some (CarrierPigeon pigeonName) }
type Services =
{
sendEmail : EmailAddress -> unit
callPhone : TelephoneNumber -> unit
sendPigeon : string -> unit
}
let contactUser services user =
match user.ContactMethod with
| Some (Email emailAddress) -> services.sendEmail emailAddress
| Some (Telephone telephoneNumber) -> services.callPhone telephoneNumber
| Some (CarrierPigeon pigeonName) -> services.sendPigeon pigeonName
| None -> failwith "Can't contact user"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment