-
|
As far as I see, the most ideal use case of DomainType, is for value objects that are only a wrapper around one property, like this: public sealed class NonEmptyString : DomainType<NonEmptyString, string>
{
private readonly string _value;
private NonEmptyString(string value)
{
_value = value;
}
public override string ToString() => _value;
public static NonEmptyString From(string repr) =>
string.IsNullOrEmpty(repr) is false
? throw new InvalidOperationException("Value cannot be empty or null")
: new NonEmptyString(repr);
public string To() => _value;
}But, how to we use DomainType with more complex value objects, like this one? public sealed class OfficeApprover : DomainType<OfficeApprover, (InternalUserId InternalUserId, string TextA, string TextB)>
{
private OfficeApprover(InternalUserId internalUserId, string textA, string textB)
{
InternalUserId = internalUserId;
TextA = textA;
TextB = textB;
}
public InternalUserId InternalUserId { get; }
public string TextA { get; }
public string TextB { get; }
public static OfficeApprover From((InternalUserId InternalUserId, string TextA, string TextB) value)
=> new(value.InternalUserId, value.TextA, value.TextB);
public (InternalUserId InternalUserId, string TextA, string TextB) To() => (InternalUserId, TextA, TextB);
} |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
|
Not sure if you've seen it yet, but the blog post that started off the domain-type traits is worth a read; it gives the motivation for the feature. In my mind, domain-types and their derivatives I don't think it's necessarily wrong to have a domain-type that represents a tuple of values -- for complex numbers, or vectors, it would work well -- but, I think, for types that probably should just be records, it might be taking the idea too far. Hands up, I haven't thought that far ahead on this feature yet, but instinctively it feels to be stretching the idea. However, from a type-theoretical point-of-view, it's completely legitimate to project a tuple onto a record, they are, after all, both product-types. So, yeah I guess you should consider what you're getting out of doing this, to decide if this is valuable or not.
|
Beta Was this translation helpful? Give feedback.
-
|
As my experience with the library grow, my knowledge grow also; and I think I have an answer to this question that I ask more than a year ago Yes, the perfect use case to DomainType<SELF, REPR> is to encapsulate one property, acting like a wrapper with more specific concept that you are trying to define. Examples of this can be anyone of the mentions above by Louthy, but I brough this examples. I think that this might work as base knowledge to know how to use all of the traits derived from DomainType public sealed class EmailAddress : DomainType<EmailAddress, string>
{
public static int MaxLength => 256;
public static int MinLength => 1;
public const string MaxLengthMessage = "El correo ingresado debe tenemos entre {0} y de {1} caracteres";
public const string DoesNotHaveAtMessage = "El correo debe tener el formato correcto";
private readonly string _value;
private EmailAddress(string value) => _value = value;
public override string ToString() => _value;
public string To() => _value;
public static Fin<EmailAddress> From(string value)
{
var lengthVal = value.Length > MaxLength || value.Length < MinLength
? Error.New(string.Format(MaxLengthMessage, MinLength, MaxLength)
: FinSucc(unit);
var atVal = not(value.Contains('@'))
? Error.New(DoesNotHaveAtMessage)
: FinSucc(unit);
return (lengthVal, atVal)
.Apply((_, _) => new EmailAddress(value))
.As();
}
}using LanguageExt.Traits;
namespace Common.ValueObjects;
public sealed record SurfaceError(double Min,
double Max,
Area Value)
: Expected(string.Empty, 0);
public sealed class Surface<Min, Max>
: DomainType<Surface<Min, Max>, Area>
where Min : Const<double>
where Max : Const<double>
{
private readonly Area _value;
private Surface(Area value) => _value = value;
public override string ToString() => $"{_value.SqMetres:N2} m\u00b2";
public Area To() => _value;
public static Fin<Surface<Min, Max>> From(Area repr)
{
Fin<Unit> minLength = repr.SqMetres < Min.Value || repr.SqMetres > Max.Value
? new SurfaceError(Min.Value, Max.Value, repr)
: unit;
return minLength.Map(_ => new Surface<Min, Max>(repr));
}
}using LanguageExt.Traits;
namespace Common.ValueObjects;
public sealed record TextError(int Min,
int Max,
int Current,
string Value)
: Expected(string.Empty, 0);
public class Text<Min, Max> : DomainType<Text<Min, Max>, string>
where Min : Const<int>
where Max : Const<int>
{
private readonly string _value;
public static int MinLength => Min.Value;
public static int MaxLength => Max.Value;
private Text(string value) => _value = value;
public override string ToString() => _value.Trim();
public static Fin<Text<Min, Max>> From(string repr)
{
repr = repr.Trim();
Fin<Unit> minLength = repr.Length < MinLength || repr.Length > MaxLength
? new TextError(Min.Value, Max.Value, repr.Length, repr)
: unit;
return minLength.Map(_ => new Text<Min, Max>(repr));
}
public string To() => _value.Trim();
}Aside of that, and extending an answer for #1448. Yes, you only need to implement Fin From(REPR), because this a trait, and a benefit of a trait is to can have extended functionality by only providing the necesary functions. Like Prelude.cs, you can create a prelude in your code that can provide you access to T.FromUnsafe indirecly, or create any other augmented functionality that you need. Take for example this public static partial class AppPrelude
{
// To facilitate use, don't use 2 generic parameters, this appPrelude must have an overload for each
// primitive type that you use in all of your DomainType<SELF, REPR> implementations
public static Fin<T> safe<T>(string repr)
where T : DomainType<T, string> =>
T.From(repr);
public static FinT<M, T> safe<M, T>(string repr)
where M : Monad<M>
where T : DomainType<T, string> =>
safe<T>(repr);
public static T @unsafe<T>(string repr)
where T : DomainType<T, string> =>
T.FromUnsafe(repr);
}So, if you add an "global using static [namespace, if required].AppPrelude;" yo you project, you can now create the implementations via these methods But what about the base trait, DomainType? What about a value object that is created by a "tuple" of 2 values? These value objects are a bit tricky, if the tuple does not has added validations, you can just use records, leveling the validation to simple value objects. For example, here is an "AuditableAction", a value object that represents the moment when an action was... actioned (?), it has who has done it (ById property) and when (At property) public sealed record AuditableAction(AccountId ById, DateTimeOffset At) : DomainType<AuditableAction2>
{
public static AuditableAction2 New(AccountId id, DateTimeOffset at) =>
new(id, at);
public static Fin<AuditableAction2> From(AccountId byId, DateTimeOffset at) =>
new AuditableAction2(byId, at);
public static Fin<AuditableAction2> From(string byIdValue, DateTimeOffset at) =>
from accountId in AccountId.From(byIdValue)
from audAct in From(accountId, at)
select audAct;
public static AuditableAction2 FromUnsafe(string byId, DateTimeOffset at) =>
From(byId, at).ThrowIfFail();
public static AuditableAction2 FromUnsafe(AccountId byId, DateTimeOffset at) =>
From(byId, at).ThrowIfFail();
public (AccountId ById, DateTimeOffset At) To() => (ById, At);
}In case that the tuple of values with added validation, to avoid invalid states you need to use a class, because with records you might create invalid instances using "with" keyword. As and example, here I have a two constants (For the numbers 1 and 99_999_999) and three ValueObjects, the first one represents the valid chars of the verification digit of the Chilean DNI, the second represents any number between a range, the last one is the Chilean DNI itself with the added validations. public sealed class I1 : Const<int>
{
public static int Value => 1;
}
public sealed class I99_999_999 : Const<int>
{
public static int Value => 99_999_999;
}
public sealed class RutVerificationDigit : DomainType<RutVerificationDigit>
{
private readonly char _value;
private RutVerificationDigit(char value) =>
_value = value;
public static Fin<RutVerificationDigit> From(char repr) =>
repr switch
{
{ } when char.IsDigit(repr) => new RutVerificationDigit(repr),
'K' or 'k' => new RutVerificationDigit('K'),
_ => Error.New("Dígito verificador inválido")
};
public char To() => _value;
}
public sealed class Number<T, Min, Max> : DomainType<Number<T, Min, Max>, T>
where T : INumber<T>
where Min : Const<T>
where Max : Const<T>
{
private readonly T _value;
public static T MinLength => Min.Value;
public static T MaxLength => Max.Value;
private Number(T value) => _value = value;
public override string ToString() => _value.ToString() ?? "";
public static Fin<Number<T, Min, Max>> From(T repr)
{
Fin<Unit> minLength = repr < MinLength || repr > MaxLength
? new NumberError(Min.Value, Max.Value, repr, repr)
: unit;
return minLength.Map(_ => new Number<T, Min, Max>(repr));
}
public T To() => _value;
}
public sealed class Rut : DomainType<Rut>
{
public const string InvalidDigitError = "El rut enviado no es valido";
public Number<int, I1, I99_999_999> Body { get; }
public RutVerificationDigit Digit { get; }
private Rut(Number<int, I1, I99_999_999> body, RutVerificationDigit digit) =>
(Body, Digit) = (body, digit);
public override string ToString() => $"{Body}-{Digit}";
public static Rut FromUnsafe(Number<int, I1, I99_999_999> body, RutVerificationDigit digit) =>
From(body, digit).ThrowIfFail();
public static Fin<Rut> From(Number<int, I1, I99_999_999> body, RutVerificationDigit digit)
{
var bodyValue = body.To();
int suma = 0, multiplicador = 1;
while (bodyValue != 0)
{
multiplicador++;
if (multiplicador == 8)
multiplicador = 2;
suma += bodyValue % 10 * multiplicador;
bodyValue /= 10;
}
suma = 11 - (suma % 11);
char validDigit = suma switch
{
11 => '0',
10 => 'K',
_ => (char)(suma + 48)
};
return validDigit == digit.To()
? FinSucc(new Rut(body, digit))
: Error.New(InvalidDigitError);
}
public static Fin<Rut> From(int bodyValue, char digitValue)
{
var body = safe<Number<int, I1, I99_999_999>>(bodyValue);
var digit = safe<RutVerificationDigit>(digitValue);
return (body, digit).Apply(From).As().Flatten();
}
public static Run FromUnsafe(int bodyValue, char digitValue) =>
From(bodyValue, digitValue).ThrowIfFail();
}As you see, you need to code the methods FromUnsafe and From, also the Prelude.safe and Prelude.@unsafe if necessary. But these methods can be source generated (except for the case value objects with added valdations), so in the future it might be free functionality. |
Beta Was this translation helpful? Give feedback.
Not sure if you've seen it yet, but the blog post that started off the domain-type traits is worth a read; it gives the motivation for the feature.
In my mind, domain-types and their derivatives
AmountLike,IdentifierLike,LocusLike,QuantityLike, andVectorSpaceare really about strongly-typing primitive-types likeint,float, etc. The idea is to support a common set of features depending on the domain-type. So, for a database ID you might useIdentifierLike, for a typed length, you might useVectorSpace, etc.I don't think it's necessarily wrong to have a domain-type that represents a tuple of values -- for complex numbers, or vectors, it would work well -- but, I think, for types that …