Skip to content

Latest commit

 

History

History
239 lines (176 loc) · 9.32 KB

README.md

File metadata and controls

239 lines (176 loc) · 9.32 KB

StrongOf Buy Me A Coffee

StrongOf helps to implement primitives as a strong type that represents a domain object (e.g. UserId, EmailAddress, etc.). It is a simple class that wraps a value and provides a few helper methods to make it easier to work with.

In contrast to other approaches, StrongOf is above all simple and performant - and not over-engineered.

Why?

This library was developed because C# did not support type abbreviations up to and including version 12.

With C# 13 we finally get Extension Types!

See GitHub proposal: Proposal: Type aliases / abbreviations / newtype

The idea

The frequent problem in code implementation is that values are not given any meaning and many methods are simply a technical string of values or data classes are just a list of types.

public class User
{    
    public  Guid    TenantId { get; set; }
    public  Guid    UserId { get; set; }
    public  string  FirstName { get; set; }
    public  string  LastName { get; set; }
    public  string  Email { get; set; }
}

A consequential problem is that there is also no compiler support if parameters are swapped. This can only be covered by complex unit tests.

// no compiler warning if you mess up the order here
public User AddUser(Guid tenantId, Guid userId, string firstName, string lastName, string email)

The idea is to use a domain-driven design approach to give specific values a meaning through their own types.

private sealed class TenantId(Guid value)    : StrongGuid<TenantId>(value) { }
private sealed class UserId(Guid value)      : StrongGuid<UserId>(value) { }
private sealed class FirstName(string value) : StrongString<FirstName>(value) { }
private sealed class LastName(string value)  : StrongString<LastName>(value) { }
private sealed class Email(string value)     : StrongString<Email>(value) { }

public class User
{    
    public  TenantId   TenantId { get; set; }
    public  UserId     UserId { get; set; }
    public  FirstName  FirstName { get; set; }
    public  LastName   LastName { get; set; }
    public  Email      Email { get; set; }
}

// with compiler warning if you mess up the order here
public User AddUser(TenantId tenantId, UserId userId, FirstName firstName, LastName lastName, Email email)

Now you are safe!

Usage

The clearest distinction to other approaches is that all StrongOf types inherit from StrongOf<T> in order to be able to implement generic approaches. Furthermore, it is possible to extend the class, e.g. to implement validations.

private sealed class UserId(Guid value) : StrongGuid<UserId>(value) { }

Usage with Json

You can just use StrongOf.Json and use one of the pre-defined converters

public class MyClass
{
    [JsonConverter(typeof(StrongGuidJsonConverter<UserId>))]
    public UserId Id { get; set; }
}

or use the JsonSerializerOptions

JsonSerializerOptions serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters =
    {
        new StrongGuidJsonConverter<UserId>()
    }
};

string jsonString = JsonSerializer.Serialize(myObject, serializeOptions);

Usage with ASP.NET Core

You can just use StrongOf.AspNetCore and use one of the pre-defined binders

public class MyBinderProvider : IModelBinderProvider
{
    private static readonly IReadOnlyDictionary<Type, Type> s_binders = new Dictionary<Type, Type>
    {
        {typeof(UserId), typeof(StrongGuidBinder<UserId>)}
        // ... more here ...
    };

    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (s_binders.TryGetValue(context.Metadata.ModelType, out Type? binderType))
        {
            return new BinderTypeModelBinder(binderType);
        }

        return null;
    }
}

You can also create a customized binder

public class MyCustomStrongGuidBinder<TStrong> : StrongOfBinder
    where TStrong : StrongGuid<TStrong>
{
    public override bool TryHandle(string value, out ModelBindingResult result)
    {
        // do something here
        
        if (StrongGuid<TStrong>.TryParse(value, out TStrong? strong))
        {
            result = ModelBindingResult.Success(strong);
            return true;
        }

        result = ModelBindingResult.Failed();
        return false;
    }
}

Usage with Entity Framework

Unfortunately, Entity Framework does not love generic Value Converters, which is why you have to write it yourself.

public class UserIdValueConverter : ValueConverter<UserId, Guid>
{
    public UserIdValueConverter(ConverterMappingHints? mappingHints = null)
        : base(id => id.Value, value => new(value), mappingHints) { }
}

There is no benefit in providing you a base class with an additional package and dependency.

Usage with FluentValidation

FluentValidation is a great library for validating models; especially popular in the ASP.NET Core world.
Therefore, separate validations are available for StrongOf models, which are constantly being expanded.

In order not to forget the namespace, separate methods are available that differ from the default ValidationContext.

public class MySubmitModel
{
    // Mandatory properties should be 
    //  marked as not null, but can still be null at 
    //  runtime if no value has been passed.
    public MyStrongString MyUserName { get; set; } = null!;
}

public class MySubmitModelValidator : AbstractValidator<MySubmitModel>
{
    public MySubmitModelValidator()
    {
        RuleFor(x => x.MyUserName)
            .HasValue() // not NotNull
            .WithMessage("No user name passed.");

        // more validations...

Installation

StrongOf
StrongOf.AspNetCore
StrongOf.Json
StrongOf.FluentValidation

See StrongOf on NuGet.org

Performance matters

Since the strong types created here can still be instantiated with new(), this also means an enormous performance advantage over libraries that have to work with Activator.CreateInstance or Expression.New.

BenchmarkDotNet v0.13.10, Windows 10 (10.0.19045.3803/22H2/2022Update)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 8.0.100
  [Host]     : .NET 7.0.14 (7.0.1423.51910), X64 RyuJIT AVX2
  DefaultJob : .NET 7.0.14 (7.0.1423.51910), X64 RyuJIT AVX2


| Method      | Mean      | Error     | StdDev    | Median    | Gen0   | Allocated |
|------------ |----------:|----------:|----------:|----------:|-------:|----------:|
| Guid_New    |  3.366 ns | 0.0998 ns | 0.0934 ns |  3.379 ns | 0.0019 |      32 B |
| Guid_From   | 12.301 ns | 1.0492 ns | 3.0936 ns | 14.184 ns | 0.0019 |      32 B |
|             |           |           |           |           |        |           |
| Int32_New   |  2.996 ns | 0.1013 ns | 0.1167 ns |  2.954 ns | 0.0014 |      24 B |
| Int32_From  |  9.829 ns | 0.6536 ns | 1.9273 ns | 10.486 ns | 0.0014 |      24 B |
|             |           |           |           |           |        |           |
| Int64_New   |  2.703 ns | 0.0867 ns | 0.0724 ns |  2.671 ns | 0.0014 |      24 B |
| Int64_From  | 11.706 ns | 0.4101 ns | 1.2093 ns | 11.976 ns | 0.0014 |      24 B |
|             |           |           |           |           |        |           |
| String_New  |  3.807 ns | 0.1205 ns | 0.1127 ns |  3.837 ns | 0.0014 |      24 B |
| String_From | 11.339 ns | 0.4997 ns | 1.4735 ns | 10.969 ns | 0.0014 |      24 B |

For certain scenarios, this library also has an Expression.New implementation (through a static From method); but not for general instantiation.

FAQ

Why no records?

Records (currently) have a few disadvantages, which is why they are not suitable for this type of class. For example, it is currently not possible to validly inherit GetHashCode. sealed on GetHashCode is only available if the record itself is sealed, which does not make sense here.

Why no Code Generator?

Code generators are great, were my first idea too, but have proven to be a disadvantage in everyday life, e.g. when implementing generic extensions / implementations. This library is based on the experience of other libraries that have tended to be too large and their disadvantages.

Why no structs?

Structs cannot be used in all scenarios, e.g. with ASP.NET Core Action parameters.


MIT LICENSE