Skip to content

fredatgithub/Thinktecture.Runtime.Extensions

 
 

Repository files navigation

Build TestResults

Thinktecture.Runtime.Extensions
Thinktecture.Runtime.Extensions.EntityFrameworkCore
Thinktecture.Runtime.Extensions.EntityFrameworkCore5
Thinktecture.Runtime.Extensions.EntityFrameworkCore6
Thinktecture.Runtime.Extensions.EntityFrameworkCore7
Thinktecture.Runtime.Extensions.Json
Thinktecture.Runtime.Extensions.Newtonsoft.Json
Thinktecture.Runtime.Extensions.MessagePack
Thinktecture.Runtime.Extensions.AspNetCore

This library provides some interfaces, classes, Roslyn Source Generators, Roslyn Analyzers and Roslyn CodeFixes for implementation of Smart Enums and Value Objects.

Documentation

See wiki for more documentation.

Requirements

  • Version 6:
    • C# 11 (or higher) for generated code
    • SDK 7.0.102 (or higher) for building projects
  • Version 5:
    • C# 9 (or higher) for generated code
    • SDK 6.0.300 (or higher) for building projects

Smart Enums

Install: Install-Package Thinktecture.Runtime.Extensions

Documentation: Smart Enums

Features:

Definition of a 2 Smart Enums without any custom properties and methods. All other features mentioned above are generated by the Roslyn Source Generators in the background.

// Smart Enum with a string as the underlying type
public sealed partial class ProductType : IEnum<string>
{
   public static readonly ProductType Groceries = new("Groceries");
   public static readonly ProductType Housewares = new("Housewares");
}

// Smart Enum with an int as the underlying type
public sealed partial class ProductGroup : IEnum<int>
{
   public static readonly ProductGroup Apple = new(1);
   public static readonly ProductGroup Orange = new(2);
}

Behind the scenes a Roslyn Source Generator, which comes with the library, generates additional code. Some of the features that are now available are ...

// a private constructor which takes the key and additional members (if we had any)
public sealed partial class ProductType : IEnum<string>
{
   public static readonly ProductType Groceries = new("Groceries");
   ...

------------

// a property for iteration over all items
IReadOnlyList<ProductType> allTypes = ProductType.Items;

------------

// getting the item with specific name, i.e. its key
// throw UnknownEnumIdentifierException if the provided key doesn't match to any item
ProductType productType = ProductType.Get("Groceries");

// Alternatively, using an explicit cast (behaves the same as with Get)
ProductType productType = (ProductType)"Groceries";

------------

// the same as above but returns a bool instead of throwing an exception (dictionary-style)
bool found = ProductType.TryGet("Groceries", out ProductType productType);

------------

// similar to TryGet but returns a ValidationResult instead of a boolean.
ValidationResult? validationResult = ProductType.Validate("Groceries", out productType);

if (validationResult == ValidationResult.Success)
{
    logger.Information("Product type {Type} found with Validate", productType);
}
else
{
    logger.Warning("Failed to fetch the product type with Validate. Validation result: {ValidationResult}", validationResult.ErrorMessage);
}

------------

// implicit conversion to the type of the key
string key = ProductType.Groceries; // "Groceries"

------------

// Equality comparison with 'Equals' 
// which compares the keys using default or custom 'IEqualityComparer<T>'
bool equal = ProductType.Groceries.Equals(ProductType.Groceries);

------------

// Equality comparison with '==' and '!='
bool equal = ProductType.Groceries == ProductType.Groceries;
bool notEqual = ProductType.Groceries != ProductType.Groceries;

------------

// Hash code
int hashCode = ProductType.Groceries.GetHashCode();

------------

// 'ToString' implementation
string key = ProductType.Groceries.ToString(); // "Groceries"

------------

ILogger logger = ...;

// Switch-case (Action)
productType.Switch(ProductType.Groceries, () => logger.Information("Switch with Action: Groceries"),
                   ProductType.Housewares, () => logger.Information("Switch with Action: Housewares"));
                   
// Switch-case with parameter (Action<TParam>) to prevent closures
productType.Switch(logger,
                   ProductType.Groceries, static l => l.Information("Switch with Action: Groceries"),
                   ProductType.Housewares, static l => l.Information("Switch with Action: Housewares"));

// Switch case returning a value (Func<TResult>)
var returnValue = productType.Switch(ProductType.Groceries, static () => "Switch with Func<T>: Groceries",
                                     ProductType.Housewares, static () => "Switch with Func<T>: Housewares");

// Switch case with parameter returning a value (Func<TParam, TResult>) to prevent closures
returnValue = productType.Switch(logger,
                                 ProductType.Groceries, static l => "Switch with Func<T>: Groceries",
                                 ProductType.Housewares, static l => "Switch with Func<T>: Housewares");

------------

// Implements IParsable<T> which is especially helpful with minimal web apis.
// This feature can be disabled if it doesn't make sense (see EnumGenerationAttribute).
bool parsed = ProductType.TryParse("Groceries", null, out var parsedProductType);

------------

// Implements IFormattable if the underlyng type (like int) is an IFormattable itself.
// This feature can be disabled if it doesn't make sense (see EnumGenerationAttribute).
var formatted = ProductGroup.Apple.ToString("000", CultureInfo.InvariantCulture); // 001

------------

// Implements IComparable and IComparable<T> if the underlyng type (like int) is an IComparable itself.
// This feature can be disabled if it doesn't make sense (see EnumGenerationAttribute).
var comparison = ProductGroup.Apple.CompareTo(ProductGroup.Orange); // -1

// Implements comparison operators (<,<=,>,>=) if the underlyng type (like int) has comparison operators itself.
// This feature can be disabled if it doesn't make sense (see EnumGenerationAttribute).
var isBigger = ProductGroup.Apple > ProductGroup.Orange;       

Definition of a new Smart Enum with 1 custom property RequiresFoodVendorLicense and 1 method Do with different behaviors for different enum items.

public partial class ProductType : IEnum<string>
{
   public static readonly ProductType Groceries = new("Groceries",  requiresFoodVendorLicense: true);
   public static readonly ProductType Housewares = new HousewaresProductType();

   public bool RequiresFoodVendorLicense { get; }

   public virtual void Do()
   {
      // do default stuff
   }

   private class HousewaresProductType : ProductType
   {
      public HousewaresProductType()
         : base("Housewares", requiresFoodVendorLicense: false)
      {
      }

      public override void Do()
      {
         // do special stuff
      }
   }
}

Value Objects

Install: Install-Package Thinktecture.Runtime.Extensions

Documentation: Value Objects

Features:

Simple Value Object

Definition of 2 value objects, one with 1 string property Value and the other with an int.

[ValueObject]
public sealed partial class ProductName
{
   public string Value { get; }
}

[ValueObject]
public sealed partial class Amount
{
   private readonly int _value;
}

After the implementation of the ProductName, a Roslyn source generator kicks in and implements the rest. Following API is available from now on.

// Factory method for creation of new instances.
// Throws ValidationException if the validation fails
ProductName bread = ProductName.Create("Bread");

// Alternatively, using an explicit cast (behaves the same as with Create)
ProductName bread = (ProductName)"Bread"; // is the same as calling "ProductName.Create"

-----------

// the same as above but returns a bool instead of throwing an exception (dictionary-style)
bool created = ProductName.TryCreate("Milk", out ProductName milk);

-----------

// similar to TryCreate but returns a ValidationResult instead of a boolean.
ValidationResult? validationResult = ProductName.Validate("Milk", out var milk);

if (validationResult == ValidationResult.Success)
{
    logger.Information("Product name {Name} created", milk);
}
else
{
    logger.Warning("Failed to create product name. Validation result: {ValidationResult}", validationResult.ErrorMessage);
}

-----------

// implicit conversion to the type of the key member
string valueOfTheProductName = bread; // "Bread"

-----------

// Equality comparison with 'Equals'
// which compares the key members using default or custom 'IEqualityComparer<T>'
bool equal = bread.Equals(bread);

-----------

// Equality comparison with '==' and '!='
bool equal = bread == bread;
bool notEqual = bread != bread;

-----------

// Hash code
int hashCode = bread.GetHashCode();

-----------

// 'ToString' implementation
string value = bread.ToString(); // "Bread"

------------

// Implements IParsable<T> which is especially helpful with minimal web apis.
// This feature can be disabled if it doesn't make sense (see ValueObjectAttribute).
bool success = ProductName.TryParse("New product name", null, out var productName);

------------

// Implements "IFormattable" if the key member is an "IFormattable".
// This feature can be disabled if it doesn't make sense (see ValueObjectAttribute).
var amount = Amount.Create(42);
string formattedValue = amount.ToString("000", CultureInfo.InvariantCulture); // "042"

------------

// Implements "IComparable<ProductName>" if the key member is an "IComparable"
// This feature can be disabled if it doesn't make sense (see ValueObjectAttribute).
var amount = Amount.Create(1);
var otherAmount = Amount.Create(2);

var comparison = amount.CompareTo(otherAmount); // -1

// Implements comparison operators (<,<=,>,>=) if the key member has comparison operators itself.
// This feature can be disabled if it doesn't make sense (see ValueObjectAttribute).
var isBigger = amount > otherAmount;  

------------

// Implements addition / subtraction / multiplication / division if the key member supports operators
// This feature can be disabled if it doesn't make sense (see ValueObjectAttribute).
var sum = amount + otherAmount;

Complex Value Objects

A complex value object is considered a class or a readonly struct with a ValueObjectAttribute and with more than 1 "assignable" properties/fields. The main use case is to manage multiple values as a whole.

A simple example would be a Boundary with 2 properties, one is the lower boundary and the other is the upper boundary. Yet again, we skip the validation at the moment.

[ValueObject]
public sealed partial class Boundary
{
   public decimal Lower { get; }
   public decimal Upper { get; }
}

The rest is implemented by a Roslyn source generator, providing the following API:

// Factory method for creation of new instances.
// Throws ValidationException if the validation fails
Boundary boundary = Boundary.Create(lower: 1, upper: 2);

-----------

// the same as above but returns a bool instead of throwing an exception (dictionary-style)
bool created = Boundary.TryCreate(lower: 1, upper: 2, out Boundary boundary);

-----------

// similar to TryCreate but returns a ValidationResult instead of a boolean.
ValidationResult? validationResult = Boundary.Validate(lower: 1, upper: 2, out Boundary boundary);

if (validationResult == ValidationResult.Success)
{
    logger.Information("Boundary {Boundary} created", boundary);
}
else
{
    logger.Warning("Failed to create boundary. Validation result: {ValidationResult}", validationResult.ErrorMessage);
}

-----------

// Equality comparison with 'Equals'
// which compares the members using default or custom comparers.
// Strings are compared with 'StringComparer.OrdinalIgnoreCase' by default.
bool equal = boundary.Equals(boundary);

-----------

// Equality comparison with '==' and '!='
bool equal = boundary == boundary;
bool notEqual = boundary != boundary;

-----------

// Hash code of the members according default or custom comparers
int hashCode = boundary.GetHashCode();

-----------

// 'ToString' implementation
string value = boundary.ToString(); // "{ Lower = 1, Upper = 2 }"

About

Provides an easy way to implement Smart Enums and Value Objects

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • C# 100.0%