https://github.com/pawelgerr/thinktecture.runtime.extensions
Provides an easy way to implement Smart Enums and Value Objects
https://github.com/pawelgerr/thinktecture.runtime.extensions
csharp dotnet dotnet-core roslyn-generator smart-enum smart-enums source-generator value-object value-objects
Last synced: 27 days ago
JSON representation
Provides an easy way to implement Smart Enums and Value Objects
- Host: GitHub
- URL: https://github.com/pawelgerr/thinktecture.runtime.extensions
- Owner: PawelGerr
- License: bsd-3-clause
- Created: 2018-02-11T16:13:46.000Z (about 8 years ago)
- Default Branch: master
- Last Pushed: 2024-04-14T09:40:28.000Z (almost 2 years ago)
- Last Synced: 2024-04-14T09:53:21.862Z (almost 2 years ago)
- Topics: csharp, dotnet, dotnet-core, roslyn-generator, smart-enum, smart-enums, source-generator, value-object, value-objects
- Language: C#
- Homepage:
- Size: 2.59 MB
- Stars: 46
- Watchers: 6
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README



[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore7/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore8/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore9/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore10/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Json/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Newtonsoft.Json/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.MessagePack/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.AspNetCore/)
[](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Swashbuckle/)
This library provides some interfaces, classes, [Roslyn Source Generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview), Roslyn Analyzers and Roslyn CodeFixes for implementation of **Smart Enums**, **Value Objects** and **Discriminated Unions**.
* [Requirements](#requirements)
* [Migrations](#migrations)
* [Smart Enums](#smart-enums)
* [Value Objects](#value-objects)
* [Discriminated Unions](#discriminated-unions)
* [Ad hoc unions](#ad-hoc-unions)
* [Regular unions](#regular-unions)
# Documentation
See [wiki](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki) for more documentation.
**Value Objects articles**:
* [Value Objects: Solving Primitive Obsession in .NET](https://www.thinktecture.com/en/net/value-objects-solving-primitive-obsession-in-net/)
* [Handling Complexity: Introducing Complex Value Objects in .NET](https://www.thinktecture.com/en/net/handling-complexity-introducing-complex-value-objects-in-dotnet/)
* [Value Objects in .NET: Integration with Frameworks and Libraries](https://www.thinktecture.com/en/net/value-objects-in-net-integration-with-frameworks-and-libraries/)
* [Value Objects in .NET: Enhancing Business Semantics](https://www.thinktecture.com/en/net/value-objects-in-dotnet-enhancing-business-semantics/)
* [Advanced Value Object Patterns in .NET](https://www.thinktecture.com/en/net/advanced-value-object-patterns-in-net/)
**Smart Enums articles**:
* [Smart Enums: Beyond Traditional Enumerations in .NET](https://www.thinktecture.com/en/net/smart-enums-beyond-traditional-enumerations-in-dotnet/)
* [Smart Enums: Adding Domain Logic to Enumerations in .NET](https://www.thinktecture.com/en/net/smart-enums-adding-domain-logic-to-enumerations-in-dotnet/)
* [Smart Enums in .NET: Integration with Frameworks and Libraries](https://www.thinktecture.com/en/net/smart-enums-in-net-integration-with-frameworks-and-libraries/)
**Discriminated Unions articles**:
* [Discriminated Unions: Representation of Alternative Types in .NET](https://www.thinktecture.com/en/net/discriminated-unions-representation-of-alternative-types-in-dotnet/)
* [Pattern Matching with Discriminated Unions in .NET](https://www.thinktecture.com/en/net/pattern-matching-with-discriminated-unions-in-net/)
* [Discriminated Unions in .NET: Modeling States and Variants](https://www.thinktecture.com/en/net/discriminated-unions-in-net-modeling-states-and-variants/)
* [Discriminated Unions in .NET: Integration with Frameworks and Libraries](https://www.thinktecture.com/en/net/discriminated-unions-in-net-integration-with-frameworks-and-libraries/)
# Requirements
* C# 11 (or higher) for generated code
* SDK 8.0.416 (or higher) for building projects
# Migrations
* [Migration from v9 to v10 (preview)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Migration-from-v9-to-v10)
* [Migration from v8 to v9](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Migration-from-v8-to-v9)
# Ideas and real-world use cases
Smart Enums:
* [Shipping Method](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#shipping-method)
* [CSV-Importer-Type](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#csv-importer-type)
* [Discriminator in a JSON Converter](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#discriminator-in-a-json-converter)
* [Dispatcher in a Web API](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#dispatcher-in-a-web-api)
Value objects:
* [ISBN](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#isbn-international-standard-book-number)
* [Open-ended End Date](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#open-ended-end-date)
* [Recurring Dates (Day-Month)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#recurring-dates-day-month)
* [Period](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#period)
* [(Always-positive) Amount](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#always-positive-amount)
* [Monetary Amount with Specific Rounding](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#monetary-amount-with-specific-rounding)
* [FileUrn - Composite Identifier with String Serialization](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#fileurn---composite-identifier-with-string-serialization)
* [Jurisdiction (combination of value objects and union types)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#jurisdiction)
Discriminated Unions:
* [Partially Known Date](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Discriminated-Unions#partially-known-date)
* [Jurisdiction (combination of value objects and union types)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Discriminated-Unions#jurisdiction)
* [Message Processing State Management](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Discriminated-Unions#message-processing-state-management)
# Smart Enums
Smart Enums provide a powerful alternative to traditional C# enums, offering type-safety, extensibility, and rich behavior.
Unlike regular C# enums which are limited to numeric values and lack extensibility, Smart Enums can:
* Use any type as the underlying type (e.g., strings, integers) or none at all
* Include additional fields, properties and behavior
* Use polymorphism to define custom behavior for each value
* Prevent creation of invalid values
* Integrate seamlessly with JSON serializers, MessagePack, Entity Framework Core, ASP.NET Core and Swashbuckle (OpenAPI)
Install: `Install-Package Thinktecture.Runtime.Extensions`
Documentation: [Smart Enums](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums)
Some of the Key Features are:
* Choice between always-valid and maybe-valid Smart Enum
* Reflection-free iteration over all items
* Fast lookup/conversion from underlying type to Smart Enum and vice versa
* Allows custom properties and methods
* Exhaustive pattern matching with `Switch`/`Map` methods
* Provides appropriate constructor, based on the specified properties/fields
* Proper implementation of `Equals`, `GetHashCode`, `ToString` and equality operators
* Provides implementation of `IComparable`, `IComparable`, `IFormattable`, `IParsable` and comparison operators `<`, `<=`, `>`, `>=` (if applicable to the underlying type)
* Custom comparer and equality comparer
Roslyn Analyzers and CodeFixes help the developers to implement the Smart Enums correctly
Provides support for:
* JSON (System.Text.Json and Newtonsoft)
* Minimal Api Parameter Binding and ASP.NET Core Model Binding
* Entity Framework Core
* MessagePack
Definition of a Smart Enum with custom properties and methods.
```C#
[SmartEnum]
public partial class ShippingMethod
{
public static readonly ShippingMethod Standard = new(
"STANDARD",
basePrice: 5.99m,
weightMultiplier: 0.5m,
estimatedDays: 5,
requiresSignature: false);
public static readonly ShippingMethod Express = new(
"EXPRESS",
basePrice: 15.99m,
weightMultiplier: 0.75m,
estimatedDays: 2,
requiresSignature: true);
public static readonly ShippingMethod NextDay = new(
"NEXT_DAY",
basePrice: 29.99m,
weightMultiplier: 1.0m,
estimatedDays: 1,
requiresSignature: true);
private readonly decimal _basePrice;
private readonly decimal _weightMultiplier;
private readonly int _estimatedDays;
public bool RequiresSignature { get; }
public decimal CalculatePrice(decimal orderWeight)
{
return _basePrice + (orderWeight * _weightMultiplier);
}
public DateTime GetEstimatedDeliveryDate()
{
return DateTime.Today.AddDays(_estimatedDays);
}
}
```
Behind the scenes a Roslyn Source Generator generates additional code. Some of the features that are now available are ...
### Basic Operations
```C#
[SmartEnum]
public partial class ProductType
{
// The source generator creates a private constructor
public static readonly ProductType Groceries = new("Groceries");
}
// Enumeration over all defined items
IReadOnlyList allTypes = ProductType.Items;
// Value retrieval
ProductType productType = ProductType.Get("Groceries"); // Get by key (throws if not found)
ProductType productType = (ProductType)"Groceries"; // Same as above but by using a cast
bool found = ProductType.TryGet("Groceries", out var productType); // Safe retrieval (returns false if not found)
// Validation with detailed error information
ValidationError? error = ProductType.Validate("Groceries", null, out ProductType? productType);
// IParsable (useful for Minimal APIs)
bool parsed = ProductType.TryParse("Groceries", null, out ProductType? parsedType);
// IFormattable (e.g. for numeric keys)
string formatted = ProductGroup.Fruits.ToString("000", CultureInfo.InvariantCulture); // "001"
// IComparable
int comparison = ProductGroup.Fruits.CompareTo(ProductGroup.Vegetables);
bool isGreater = ProductGroup.Fruits > ProductGroup.Vegetables; // Comparison operators
```
### Type Conversion and Equality
```C#
// Implicit conversion to key type
string key = ProductType.Groceries; // Returns "Groceries"
// Equality comparison
bool equal = ProductType.Groceries.Equals(ProductType.Groceries);
bool equal = ProductType.Groceries == ProductType.Groceries; // Operator overloading
bool notEqual = ProductType.Groceries != ProductType.Housewares;
// Methods inherited from Object
int hashCode = ProductType.Groceries.GetHashCode();
string key = ProductType.Groceries.ToString(); // Returns "Groceries"
// TypeConverter
var converter = TypeDescriptor.GetConverter(typeof(ProductType));
string? keyStr = (string?)converter.ConvertTo(ProductType.Groceries, typeof(string));
ProductType? converted = (ProductType?)converter.ConvertFrom("Groceries");
```
### Pattern Matching with Switch/Map
All `Switch`/`Map` methods are exhaustive by default ensuring all cases are handled correctly.
```C#
ProductType productType = ProductType.Groceries;
// Execute different actions based on the enum value (void return)
productType.Switch(
groceries: () => Console.WriteLine("Processing groceries order"),
housewares: () => Console.WriteLine("Processing housewares order")
);
// Transform enum values into different types
string department = productType.Switch(
groceries: () => "Food and Beverages",
housewares: () => "Home and Kitchen"
);
// Direct mapping to values - clean and concise
decimal discount = productType.Map(
groceries: 0.05m, // 5% off groceries
housewares: 0.10m // 10% off housewares
);
```
For optimal performance Smart Enums provide overloads that prevent closures.
```csharp
ILogger logger = ...;
// Prevent closures by passing the parameter as first method argument
productType.Switch(logger,
groceries: static l => l.LogInformation("Processing groceries order"),
housewares: static l => l.LogInformation("Processing housewares order")
);
// Use a tuple to pass multiple values
var context = (Logger: logger, OrderId: "123");
productType.Switch(context,
groceries: static ctx => ctx.Logger.LogInformation("Processing groceries order {OrderId}", ctx.OrderId),
housewares: static ctx => ctx.Logger.LogInformation("Processing housewares order {OrderId}", ctx.OrderId)
);
```
# Value Objects
Install: `Install-Package Thinktecture.Runtime.Extensions`
Documentation: [Value Objects](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects)
Value objects help solve several common problems in software development:
1. **Type Safety**: Prevent mixing up different concepts that share the same primitive type
```csharp
// Problem: Easy to accidentally swap parameters
void ProcessOrder(int customerId, int orderId) { ... }
ProcessOrder(orderId, customerId); // Compiles but wrong!
// Solution: Value objects make it type-safe
[ValueObject]
public partial struct CustomerId { }
[ValueObject]
public partial struct OrderId { }
void ProcessOrder(CustomerId customerId, OrderId orderId) { ... }
ProcessOrder(orderId, customerId); // Won't compile!
```
2. **Built-in Validation**: Ensure data consistency at creation time
```csharp
[ValueObject]
public partial struct Amount
{
static partial void ValidateFactoryArguments(ref ValidationError? validationError, ref decimal value)
{
if (value < 0)
{
validationError = new ValidationError("Amount cannot be negative");
return;
}
// Normalize to two decimal places
value = Math.Round(value, 2);
}
}
var amount = Amount.Create(100.50m); // Success: 100.50
var invalid = Amount.Create(-50m); // Throws ValidationException
```
3. **Immutability**: Prevent accidental modifications and ensure thread safety
4. **Complex Value Objects**: Encapsulate multiple related values with validation
```csharp
[ComplexValueObject]
public partial class DateRange
{
public DateOnly Start { get; }
public DateOnly End { get; }
static partial void ValidateFactoryArguments(
ref ValidationError? validationError,
ref DateOnly start,
ref DateOnly end)
{
if (end < start)
{
validationError = new ValidationError(
$"End date '{end}' cannot be before start date '{start}'");
return;
}
// Ensure dates are not in the past
var today = DateOnly.FromDateTime(DateTime.Today);
if (start < today)
{
validationError = new ValidationError("Start date cannot be in the past");
return;
}
}
public int DurationInDays => End.DayNumber - Start.DayNumber + 1;
public bool Contains(DateOnly date) => date >= Start && date <= End;
}
// Usage
var range = DateRange.Create(
start: DateOnly.FromDateTime(DateTime.Today),
end: DateOnly.FromDateTime(DateTime.Today.AddDays(7))
);
Console.WriteLine(range.DurationInDays); // 8
Console.WriteLine(range.Contains(range.Start)); // true
```
Key Features:
* Two types of value objects:
* Simple value objects (wrapper around a single value with validation)
* Complex value objects (multiple properties representing a single concept)
* Comprehensive validation support with descriptive error messages
* Framework integration:
* JSON serialization (System.Text.Json and Newtonsoft.Json)
* Entity Framework Core support
* ASP.NET Core Model Binding
* Swashbuckle (OpenAPI)
* MessagePack serialization
* Rich feature set:
* Type conversion and comparison operators
* Custom equality comparison
* Proper implementation of standard interfaces (IComparable, IFormattable, etc.)
* Configurable null and empty string handling
* Development support:
* Roslyn Analyzers and CodeFixes for correct implementation
* Logging for debugging and insights
For more examples and detailed documentation, see the [wiki](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects).
# Discriminated Unions
Install: `Install-Package Thinktecture.Runtime.Extensions`
Documentation: [Discriminated Unions](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Discriminated-Unions)
Discriminated unions are a powerful feature that allows a type to hold a value that could be one of several different types. They provide type safety, exhaustive pattern matching, and elegant handling of complex domain scenarios. Key benefits include:
* Type-safe representation of values that can be one of several types
* Exhaustive pattern matching ensuring all cases are handled
* Elegant modeling of domain concepts with multiple states
* Clean handling of success/failure scenarios without exceptions
The library provides two types of unions to suit different needs:
## Ad hoc unions
Perfect for simple scenarios where you need to combine a few types quickly. Features:
* Type-safe combination of up to 5 different types
* Implicit conversions and type checking
* Exhaustive pattern matching with Switch/Map methods
* Built-in equality comparison
* Support for class, struct, or ref struct implementations
```csharp
// Quick combination of types
[Union]
public partial class TextOrNumber;
// Create and use the union
TextOrNumber value = "Hello"; // Implicit conversion
TextOrNumber number = 42; // Works with any defined type
// Type-safe access
if (value.IsString)
{
string text = value.AsString; // Type-safe access
Console.WriteLine(text);
}
// Exhaustive pattern matching
var result = value.Switch(
@string: text => $"Text: {text}",
int32: num => $"Number: {num}"
);
// Custom property names for clarity
[Union(T1Name = "Text", T2Name = "Number")]
public partial class BetterNamed;
// Now use .IsText, .IsNumber, .AsText, .AsNumber
```
## Regular unions
Ideal for modeling domain concepts and complex hierarchies. Features:
* Inheritance-based approach for complex scenarios
* Support for both classes and records
* Integration with value objects
* Generic type support
* Exhaustive pattern matching
Perfect for modeling domain concepts:
```csharp
// Model domain concepts clearly
[Union]
public partial record OrderStatus
{
public record Pending : OrderStatus;
public record Processing(DateTime StartedAt) : OrderStatus;
public record Completed(DateTime CompletedAt, string TrackingNumber) : OrderStatus;
public record Cancelled(string Reason) : OrderStatus;
}
// Generic result type for error handling
[Union]
public partial record Result
{
public record Success(T Value) : Result;
public record Failure(string Error) : Result;
// Implicit conversions from T and string are implemented automatically
}
// Usage
Result result = await GetDataAsync();
var message = result.Switch(
success: s => $"Got value: {s.Value}",
failure: f => $"Error: {f.Error}"
);
```