-
Notifications
You must be signed in to change notification settings - Fork 1
Home
This library provides some interfaces, classes, Roslyn Source Generators, Roslyn Analyzers and Roslyn CodeFixes for implementation of Smart Enums and Value Objects.
- Smart Enums
- Value objects
- Convenience methods and classes
- 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
Install: Install-Package Thinktecture.Runtime.Extensions
Documentation: Smart Enums
Features:
- Roslyn Analyzers and CodeFixes help the developers to implement the Smart Enums correctly
- Allows iteration over all items
- Allows custom properties and methods
- Switch-case
- Provides appropriate constructor, based on the specified properties/fields
- Provides means for lookup, cast and type conversion from key-type to Smart Enum and vice versa
- Provides proper implementation of
Equals
,GetHashCode
,ToString
and equality comparison via==
and!=
- Provides implementation of
IComparable
,IComparable<T>
,IFormattable
,IParsable<T>
and comparison operators<
,<=
,>
,>=
(if applicable to the underlying type) - [Choice between always-valid Smart Enum and maybe-valid one(Smart-Enums#ienumt-vs-ivalidatableenumt)
- Makes use of abstract static members
- Derived types can be generic
- Allows custom validation of constructor arguments
- Allows changing the property name
Key
- Allows custom key comparer
-
JSON support (
System.Text.Json
andNewtonsoft.Json
) - Support for Minimal Web Api Parameter Binding and ASP.NET Core Model Binding
-
Entity Framework Core support (
ValueConverter
) -
MessagePack support (
IMessagePackFormatter
)
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 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.
[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
}
}
}
Install: Install-Package Thinktecture.Runtime.Extensions
Documentation: Value Objects
Features:
- Roslyn Analyzers and CodeFixes help the developers to implement the Value Objects correctly
- Allows custom properties and methods
- Provides appropriate factory methods for creation of new value objects based on the specified properties/fields
- Allows custom validation of constructor and factory method arguments
- Additional features for simple Value Objects (1 "key"-property/field) and complex Value Objects (2 properties/fields or more)
- Simple Value Objects: allows cast and type conversion from key-type to Value Object and vice versa
- Simple Value Objects: provides an implementation of
IFormattable
if the key-property/field is anIFormattable
- Provides proper implementation of
Equals
,GetHashCode
,ToString
and equality comparison via==
and!=
- Provides implementation of
IComparable
,IComparable<T>
,IFormattable
,IParsable<T>
and comparison operators<
,<=
,>
,>=
- Allows custom equality comparison and custom comparer
- Handling of null and empty strings
-
JSON support (
System.Text.Json
andNewtonsoft.Json
) - Support for Minimal Web Api Parameter Binding and ASP.NET Core Model Binding
-
Entity Framework Core support (
ValueConverter
) -
MessagePack support (
IMessagePackFormatter
)
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;
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}'");
}
}