Giter VIP home page Giter VIP logo

wrappervalueobject's Introduction

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.

wrappervalueobject's People

Contributors

martinothamar avatar viceroypenguin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

wrappervalueobject's Issues

Integer Id object

If I have an int ProductId, then I don't want the math operator overloads to be provided. Recommendations for improvement:

  • If the object ends w/ Id, then don't add the math operators by default
  • Add a flag to the Attribute that allows override (I do or do not want math operators)

[Question / Bug] Is it possible to have default values?

Is it possible to have default values? I noticed that .Validate() does not run when the constructor is empty. For example:

Scenerio

This example is for PageNumber - such as a 'PageNumber' that is used in Pagination.

// ~~ Page Number - Requirements ~~
// -> The PageNumber Cannot be 0 or a negative number
// -> Should throw if the PageNumber is 0 or a negative number

[WrapperValueObject(typeof(int))]
public readonly partial struct PageNumber
{
   static partial void Validate(int value)
    {
        // If the pageNumber is 0 or a negative number, an exception should be thrown.
        if (value <= 0)
        {
            throw new Exception();
        }
    }
}

Example Unit Tests

// Pass
[Fact]
public void ShouldNotThrowWhenNumberIsGreaterThanZero_Value_One()
{
     var currentPageNumber = new PageNumber(1);
     Assert.Equal(1, currentPageNumber.Value);
 }

 // Pass
[Fact]
public void ShouldThrowWhenLessThanZero_Value_NegativeOne()
{
    Assert.Throws<Exception>(() => new PageNumber(-1));
}

//-----------------------------------
//------- Failing Test Below -------
//-----------------------------------

// Fail
[Fact]
public void ShouldThrowWhenLessThanZero_Value_Empty()
{
    Assert.Throws<Exception>(() => new PageNumber());
}

Question

When a user performs:

var somePageNumber = new PageNumber(); // PageNumber == 0

Is there a way to call .Validate() on an empty constructor?

Thanks

Feature request: Generate partial validation method

It would be appropriate to generate partial methods for the validation of the value.

Code:

[WrapperValueObject(typeof(decimal))] readonly partial struct Money { }

Generates eg.:

partial struct Money 
{
    private decimal _value;

    
    public Money(decimal value)
    {
         Validate(value);
         _value = value;
    } 

   partial void Validate(decimal value);
   // ...
}

And if the consumer of api will want to implement value check/validation, then implements method Validate.

[WrapperValueObject(typeof(decimal))] 
readonly partial struct Money 
{
    partial void Validate(decimal value)
    {
         if(value<0.0) throw new ArgumentException("Money value can not by less than zero.");
    }
 }

Use cases:

  • Databse IDs (must by more than zero),
  • String keys in specific format (for nosql databases),
  • Percent (0-100%),
  • Special units (PH, °C,...)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.