Skip to content

Repository files navigation

WrapperValueObject

BuildNuGet

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 to int
    • Math and comparison operator overloads are automatically generated
    • ToString is generated with formatting options similar to those on the primitive type, i.e. ToString(string? format, IFormatProvider? provider) for math types
  • Strongly typed ID's
    • Similar to F# type ProductId = ProductId of Guid, here it becomes [WrapperValueObject] readonly partial struct ProductId { } with a New() function similar to Guid.NewGuid()

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.

Installation

Add to your project file:

<PackageReferenceInclude="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.1

This package is a build time dependency only.

Usage

  1. Use the attribute to specify the underlying type.
  2. Declare the struct or class with the partial keyword.

Strongly typed ID

[WrapperValueObject]readonlypartialstructProductId{}varid=ProductId.New();// Strongly typed Guid wrapper, i.e. {1658db8c-89a4-46ea-b97e-8cf966cfb3f1}Assert.NotEqual(ProductId.New(),id);Assert.False(ProductId.New()==id);

Money type

[WrapperValueObject(typeof(decimal))]readonlypartialstructMoney{}Moneymoney=2m;varresult=money+2m;// 4.0varresult2=money+newMoney(2m);Assert.True(result==result2);Assert.Equal(4m,(decimal)result);

Metric types

[WrapperValueObject(typeof(int))]publicreadonlypartialstructMeterLength{publicstaticimplicitoperatorCentimeterLength(MeterLengthmeter)=>meter.Value*100;// .Value is the inner type, in this case int}[WrapperValueObject(typeof(int))]publicreadonlypartialstructCentimeterLength{publicstaticimplicitoperatorMeterLength(CentimeterLengthcentiMeter)=>centiMeter.Value/100;}MeterLengthmeters=2;CentimeterLengthcentiMeters=meters;// 200Assert.Equal(200,(int)centiMeters);

Complex types

[WrapperValueObject]// Is Guid ID by defaultreadonlypartialstructMatchId{}[WrapperValueObject("HomeGoals",typeof(byte),"AwayGoals",typeof(byte))]readonlypartialstructMatchResult{}partialstructMatch{publicreadonlyMatchIdMatchId{get;}publicMatchResultResult{get;privateset;}publicvoidSetResult(MatchResultresult)=>Result=result;publicMatch(inMatchIdmatchId){MatchId=matchId;Result=default;}}varmatch=newMatch(MatchId.New());match.SetResult((1,2));// Complex types use value tuples underneath, so can be implicitly convertedmatch.SetResult(newMatchResult(1,2));// Or the full constructorvarotherResult=newMatchResult(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);

Validation

To make sure only valid instances are created. The validate function will be called in the generated constructors.

[WrapperValueObject]// Is Guid ID by defaultreadonlypartialstructMatchId{staticpartialvoidValidate(Guidid){if(id==Guid.Empty)thrownewArgumentOutOfRangeException(nameof(id),$"{nameof(id)} must have value");}}[WrapperValueObject("HomeGoals",typeof(byte),"AwayGoals",typeof(byte))]readonlypartialstructMatchResult{staticpartialvoidValidate(bytehomeGoals,byteawayGoals){if(homeGoals<0)thrownewArgumentOutOfRangeException(nameof(homeGoals),$"{nameof(homeGoals)} value cannot be less than 0");if(awayGoals<0)thrownewArgumentOutOfRangeException(nameof(awayGoals),$"{nameof(awayGoals)} value cannot be less than 0");}}

Limitations

  • 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

Related projects and inspiration

TODO/under consideration

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.

About

A .NET source generator for creating simple value objects wrapping primitive types.

Topics

Resources

Stars

52 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages