Skip to content

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

License

Notifications You must be signed in to change notification settings

martinothamar/WrapperValueObject

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WrapperValueObject

Build NuGet

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:

<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.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] 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);

Money type

[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);

Metric types

[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);

Complex types

[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);

Validation

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");
    }
}

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.