Skip to content
Pawel Gerr edited this page Nov 8, 2023 · 46 revisions

Build TestResults

Thinktecture.Runtime.Extensions
Thinktecture.Runtime.Extensions.EntityFrameworkCore5
Thinktecture.Runtime.Extensions.EntityFrameworkCore6
Thinktecture.Runtime.Extensions.EntityFrameworkCore7
Thinktecture.Runtime.Extensions.EntityFrameworkCore8
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.

Requirements

  • Version 7:
    • C# 11 (or higher) for generated code
    • SDK 7.0.401 (or higher) for building projects
  • 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

Migrations

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
[SmartEnum<string>]
public sealed partial class ProductType
{
   public static readonly ProductType Groceries = new("Groceries");
   public static readonly ProductType Housewares = new("Housewares");
}

// Smart Enum with an int as the underlying type
[SmartEnum<int>]
public sealed partial class ProductGroup
{
   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)
[SmartEnum<string>]
public sealed partial class ProductType
{
   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 accepts `IFormatProvider` and returns a ValidationResult instead of a boolean.
ValidationResult? validationResult = ProductType.Validate("Groceries", null, 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.

[SmartEnum<string>]
public partial class ProductType
{
   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 accepts `IFormatProvider` and returns a ValidationResult instead of a boolean.
ValidationResult? validationResult = ProductName.Validate("Milk", null, 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 Object

Definition of a complex value object with 2 properties and a custom validation of the arguments.

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

   static partial void ValidateFactoryArguments(ref ValidationResult? validationResult, ref decimal lower, ref decimal upper)
   {
      if (lower <= upper)
         return;

      validationResult = new ValidationResult($"Lower boundary '{lower}' must be less than upper boundary '{upper}'");
   }
}