One of the core principles of DDD is the usage of value objects to avoid “primitive obsession”. "Primitives" refer to the built-in types in C#, int
, string,guid
etc. "Primitive obsession" refers to over-using these types to represent domain concepts that aren't a perfect fit. Some examples are a HouseNumber that is represented by an int or an EmailAddress that is represented by a string.
This concept not only makes sense for your value objects but is also valuable for your Entity and AggregateRoot id’s. A ProductId should not be interchangeable with an OrderId.
Creating a valueobject for every Id type is not that hard but remains cumbersome. Let’s introduce StronglyTypedId as a solution.
From the website:
StronglyTypedId makes creating strongly-typed IDs as easy as adding an attribute! No more accidentally passing arguments in the wrong order to methods - StronglyTypedId uses Roslyn-powered build-time code generation to generate the boilerplate required to use strongly-typed IDs.
Getting started
- To get started, first add the StronglyTypedId nuget package to your project: https://www.nuget.org/packages/StronglyTypedId/
- Now create a struct type and add the StronglyTypedId attribute on it.
- Notice that we specify to not generate a JsonConverter. If you want to serialize the type you need to add an extra reference to Newtonsoft.Json or System.Text.Json.
- Build your project. Let’s have a look at what is generated:
- By default a Guid is used as the backing field. If you want to use a different type, you can specify this:
[StronglyTypedId(generateJsonConverter:false)] | |
public partial struct ProductID { }; | |
public class Product { | |
public ProductID ProductID { get; set; } | |
} |
[System.ComponentModel.TypeConverter(typeof(ProductIDTypeConverter))] | |
readonly partial struct ProductID : System.IComparable<ProductID>, System.IEquatable<ProductID> | |
{ | |
public System.Guid Value | |
{ | |
get; | |
} | |
public ProductID(System.Guid value) | |
{ | |
Value = value; | |
} | |
public static ProductID New() => new ProductID(System.Guid.NewGuid()); | |
public static readonly ProductID Empty = new ProductID(System.Guid.Empty); | |
public bool Equals(ProductID other) => this.Value.Equals(other.Value); | |
public int CompareTo(ProductID other) => Value.CompareTo(other.Value); | |
public override bool Equals(object obj) | |
{ | |
if (ReferenceEquals(null, obj)) | |
return false; | |
return obj is ProductID other && Equals(other); | |
} | |
public override int GetHashCode() => Value.GetHashCode(); | |
public override string ToString() => Value.ToString(); | |
public static bool operator ==(ProductID a, ProductID b) => a.CompareTo(b) == 0; | |
public static bool operator !=(ProductID a, ProductID b) => !(a == b); | |
class ProductIDTypeConverter : System.ComponentModel.TypeConverter | |
{ | |
public override bool CanConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Type sourceType) | |
{ | |
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); | |
} | |
public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) | |
{ | |
var stringValue = value as string; | |
if (!string.IsNullOrEmpty(stringValue) && System.Guid.TryParse(stringValue, out var guid)) | |
{ | |
return new ProductID(guid); | |
} | |
return base.ConvertFrom(context, culture, value); | |
} | |
} | |
} |
[StronglyTypedId(generateJsonConverter:false, backingType:StronglyTypedIdBackingType.Int)] | |
public partial struct ProductID { }; |