Note
This library is not actively maintained at the moment, I recommend looking at SteveDunn/Vogen
A .NET source generator for creating
- Simple value objects wrapping other type(s), without the hassle of manual
Equals/GetHashCode - Value objects wrapping math primitives and other types
- I.e.
[WrapperValueObject(typeof(int))] readonly partial struct MeterLength { }- the type is implicitly castable toint - Math and comparison operator overloads are automatically generated
ToStringis generated with formatting options similar to those on the primitive type, i.e.ToString(string? format, IFormatProvider? provider)for math types
- I.e.
- Strongly typed ID's
- Similar to F#
type ProductId = ProductId of Guid, here it becomes[WrapperValueObject] readonly partial struct ProductId { }with aNew()function similar toGuid.NewGuid()
- Similar to F#
The generator targets .NET Standard 2.0 and has been tested with netcoreapp3.1 and net5.0 target frameworks.
Note that record type feature for structs is planned for C# 10, at which point this library might be obsolete.
Add to your project file:
<PackageReference Include="WrapperValueObject.Generator" Version="0.0.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>Or install via CLI
dotnet add package WrapperValueObject.Generator --version 0.0.1This package is a build time dependency only.
- Use the attribute to specify the underlying type.
- Declare the struct or class with the
partialkeyword.
[WrapperValueObject] readonly partial struct ProductId { } var id = ProductId.New(); // Strongly typed Guid wrapper, i.e. {1658db8c-89a4-46ea-b97e-8cf966cfb3f1} Assert.NotEqual(ProductId.New(), id); Assert.False(ProductId.New() == id);[WrapperValueObject(typeof(decimal))] readonly partial struct Money { } Money money = 2m; var result = money + 2m; // 4.0 var result2 = money + new Money(2m); Assert.True(result == result2); Assert.Equal(4m, (decimal)result);[WrapperValueObject(typeof(int))] public readonly partial struct MeterLength { public static implicit operator CentimeterLength(MeterLength meter) => meter.Value * 100; // .Value is the inner type, in this case int } [WrapperValueObject(typeof(int))] public readonly partial struct CentimeterLength { public static implicit operator MeterLength(CentimeterLength centiMeter) => centiMeter.Value / 100; } MeterLength meters = 2; CentimeterLength centiMeters = meters; // 200 Assert.Equal(200, (int)centiMeters);[WrapperValueObject] // Is Guid ID by default readonly partial struct MatchId { } [WrapperValueObject("HomeGoals", typeof(byte), "AwayGoals", typeof(byte))] readonly partial struct MatchResult { } partial struct Match { public readonly MatchId MatchId { get; } public MatchResult Result { get; private set; } public void SetResult(MatchResult result) => Result = result; public Match(in MatchId matchId) { MatchId = matchId; Result = default; } } var match = new Match(MatchId.New()); match.SetResult((1, 2)); // Complex types use value tuples underneath, so can be implicitly converted match.SetResult(new MatchResult(1, 2)); // Or the full constructor var otherResult = new MatchResult(2, 1); Debug.Assert(otherResult != match.Result); match.SetResult((2, 1)); Debug.Assert(otherResult == match.Result); Debug.Assert(match.MatchId != default); Debug.Assert(match.Result != default); Debug.Assert(match.Result.HomeGoals == 2); Debug.Assert(match.Result.AwayGoals == 1);To make sure only valid instances are created. The validate function will be called in the generated constructors.
[WrapperValueObject] // Is Guid ID by default readonly partial struct MatchId { static partial void Validate(Guid id) { if (id == Guid.Empty) throw new ArgumentOutOfRangeException(nameof(id), $"{nameof(id)} must have value"); } } [WrapperValueObject("HomeGoals", typeof(byte), "AwayGoals", typeof(byte))] readonly partial struct MatchResult { static partial void Validate(byte homeGoals, byte awayGoals) { if (homeGoals < 0) throw new ArgumentOutOfRangeException(nameof(homeGoals), $"{nameof(homeGoals)} value cannot be less than 0"); if (awayGoals < 0) throw new ArgumentOutOfRangeException(nameof(awayGoals), $"{nameof(awayGoals)} value cannot be less than 0"); } }- Need .NET 5 SDK (I think) due to source generators
- Does not support nested types
- Limited configuration options in terms of what code is generated
- StronglyTypedId by @andrewlock
Further development on this PoC was prompted by this discussion: ironcev/awesome-roslyn#17
- Replace one generic attribute (WrapperValueObject) with two (or more) that cleary identify the usecase. E.g. StronglyTypedIdAttribute, ImmutableStructAttribute, ...
- Support everything that StronglyTypedId supports (e.g. optional generation of JSON converters).
- Bring the documentation to the same level as in the StronglyTypedId project.
- Write tests.
- Create Nuget package.