Source generators for Thinktecture.Runtime.Extensions.
This library provides some interfaces, classes, Roslyn Source Generators, Roslyn Analyzers and Roslyn CodeFixes for implementation of Smart Enums, Value Objects and Discriminated Unions.
See wiki for more documentation.
Value Objects articles:
Smart Enums articles:
Discriminated Unions articles:
Smart Enums:
Value objects:
Discriminated Unions:
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:
Install: Install-Package Thinktecture.Runtime.Extensions
Documentation: Smart Enums
Some of the Key Features are:
Switch/Map methodsEquals, GetHashCode, ToString and equality operatorsIComparable, IComparable<T>, IFormattable, IParsable<T> and comparison operators <, <=, >, >= (if applicable to the underlying type)Roslyn Analyzers and CodeFixes help the developers to implement the Smart Enums correctly
Provides support for:
Definition of a Smart Enum with custom properties and methods.
[SmartEnum<string>]
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 ...
[SmartEnum<string>]
public partial class ProductType
{
// The source generator creates a private constructor
public static readonly ProductType Groceries = new("Groceries");
}
// Enumeration over all defined items
IReadOnlyList<ProductType> 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<T> (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// 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");All Switch/Map methods are exhaustive by default ensuring all cases are handled correctly.
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.
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)
);Install: Install-Package Thinktecture.Runtime.Extensions
Documentation: Value Objects
Value objects help solve several common problems in software development:
Type Safety: Prevent mixing up different concepts that share the same primitive type
// 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<int>]
public partial struct CustomerId { }
[ValueObject<int>]
public partial struct OrderId { }
void ProcessOrder(CustomerId customerId, OrderId orderId) { ... }
ProcessOrder(orderId, customerId); // Won't compile!
Built-in Validation: Ensure data consistency at creation time
[ValueObject<decimal>]
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
Immutability: Prevent accidental modifications and ensure thread safety
Complex Value Objects: Encapsulate multiple related values with validation
[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:
For more examples and detailed documentation, see the wiki.
Install: Install-Package Thinktecture.Runtime.Extensions
Documentation: 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:
The library provides two types of unions to suit different needs:
Perfect for simple scenarios where you need to combine a few types quickly. Features:
// Quick combination of types
[Union<string, int>]
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<string, int>(T1Name = "Text", T2Name = "Number")]
public partial class BetterNamed;
// Now use .IsText, .IsNumber, .AsText, .AsNumberIdeal for modeling domain concepts and complex hierarchies. Features:
Perfect for modeling domain concepts:
// 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<T>
{
public record Success(T Value) : Result<T>;
public record Failure(string Error) : Result<T>;
// Implicit conversions from T and string are implemented automatically
}
// Usage
Result<int> result = await GetDataAsync();
var message = result.Switch(
success: s => $"Got value: {s.Value}",
failure: f => $"Error: {f.Error}"
);