Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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: 17 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 (almost 7 years ago)
- Default Branch: master
- Last Pushed: 2024-04-14T09:40:28.000Z (10 months ago)
- Last Synced: 2024-04-14T09:53:21.862Z (10 months 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
![Build](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/workflows/CI/badge.svg?branch=master)
![TestResults](https://gist.githubusercontent.com/PawelGerr/043909cfb348b36187d02222da1f372e/raw/badge.svg)
![NuGet Downloads](https://img.shields.io/nuget/dt/Thinktecture.Runtime.Extensions)[![Thinktecture.Runtime.Extensions](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.svg?maxAge=60&label=Thinktecture.Runtime.Extensions)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions/)
[![Thinktecture.Runtime.Extensions.EntityFrameworkCore6](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore6.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore6)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore6/)
[![Thinktecture.Runtime.Extensions.EntityFrameworkCore7](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore7.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore7)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore7/)
[![Thinktecture.Runtime.Extensions.EntityFrameworkCore8](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore8.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore8)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore8/)
[![Thinktecture.Runtime.Extensions.EntityFrameworkCore9](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore9.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore9)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore9/)
[![Thinktecture.Runtime.Extensions.Json](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.Json.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.Json)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Json/)
[![Thinktecture.Runtime.Extensions.Newtonsoft.Json](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.Newtonsoft.Json.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.Newtonsoft.Json)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Newtonsoft.Json/)
[![Thinktecture.Runtime.Extensions.MessagePack](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.MessagePack.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.MessagePack)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.MessagePack/)
[![Thinktecture.Runtime.Extensions.AspNetCore](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.AspNetCore.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.AspNetCore)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.AspNetCore/)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 Enum](#smart-enums)
* [Value Objects](#value-objects)
* [Discriminated Unions](#discriminated-unions) (requires version 8.x.x)
* [Ad hoc unions](#ad-hoc-unions)
* [Regular unions](#regular-unions)# Documentation
See [wiki](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki) for more documentation.
## **[Ideas and real-world use cases](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#real-world-use-cases-and-ideas)**
Smart Enums:
* [CSV-Importer-Type](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#csv-importer-type)
Value objects:
* [Open-ended End Date](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#open-ended-end-date)
* [(Always-positive) Amount](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#always-positive-amount)# Requirements
* Version 8:
* C# 11 (or higher) for generated code
* SDK 8.0.400 (or higher) for building projects
* Version 7:
* C# 11 (or higher) for generated code
* SDK 7.0.401 (or higher) for building projects# Migrations
* [Migration from v7 to v8](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Migration-from-v7-to-v8)
* [Migration from v6 to v7](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Migration-from-v6-to-v7)# Smart Enums
Install: `Install-Package Thinktecture.Runtime.Extensions`
Documentation: [Smart Enums](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums)
Features:
* Roslyn Analyzers and CodeFixes help the developers to implement the Smart Enums correctly
* [Allows iteration over all items](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#what-is-implemented-for-you)
* [Allows custom properties and methods](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#custom-fields-properties-and-methods)
* [Switch-case/Map](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#switch-casemap)
* 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`, `IFormattable`, `IParsable` and comparison operators `<`, `<=`, `>`, `>=` (if applicable to the underlying type)
* [Choice between always-valid and maybe-valid Smart Enum](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#always-valid-vs-maybe-valid-smart-enum)
* Smart Enum can also be keyless, i.e. without a key member
* [Makes use of abstract static members](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#make-use-of-abstract-static-members)
* [Derived types can be generic](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#generic-derived-types)
* [Allows custom validation of constructor arguments](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#validation-of-the-constructor-arguments)
* [Allows changing the key member name, kind and access modifier](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#key-member-generation), which holds the underlying value - thanks to [Roslyn Source Generator](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview)
* Allows [custom key equality comparer](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#custom-equality-comparer) and [custom comparer](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#custom-comparer)
* [JSON support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#json-serialization) (`System.Text.Json` and `Newtonsoft.Json`)
* [Support for Minimal Api Parameter Binding and ASP.NET Core Model Binding](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#support-for-minimal-api-parameter-binding-and-aspnet-core-model-binding)
* [Entity Framework Core support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#support-for-entity-framework-core) (`ValueConverter`)
* [MessagePack support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#messagepack-serialization) (`IMessagePackFormatter`)
* [Logging for debugging or getting insights](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Smart-Enums#logging-v610-or-higher)Definition of a 2 Smart Enums without any custom properties and methods. All other features mentioned above are generated by the [Roslyn Source Generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) in the background.
```C#
// Smart Enum with a string as the underlying type
[SmartEnum]
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]
public sealed partial class ProductGroup
{
public static readonly ProductGroup Apple = new(1);
public static readonly ProductGroup Orange = new(2);
}// Smart Enum without identifier (keyless)
[SmartEnum]
public sealed partial class SalesCsvImporterType
{
public static readonly SalesCsvImporterType Daily = new(articleIdIndex: 0, volumeIndex: 2);
public static readonly SalesCsvImporterType Monthly = new(articleIdIndex: 2, volumeIndex: 0);public int ArticleIdIndex { get; }
public int VolumeIndex { get; }
}
```Behind the scenes a Roslyn Source Generator generates additional code. Some of the features that are now available are ...
```C#
// A private constructor which takes the key "Groceries" and additional members (if we had any)
[SmartEnum]
public sealed partial class ProductType
{
public static readonly ProductType Groceries = new("Groceries");
...------------
// A property for iteration over all items
IReadOnlyList allTypes = ProductType.Items;------------
// Getting the item with specific name, i.e. its key.
// Throws UnknownEnumIdentifierException if the provided key doesn't belong to any item
ProductType productType = ProductType.Get("Groceries");// Alternatively, using an explicit cast (behaves the same as "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 ValidationError instead of a boolean.
ValidationError? validationError = ProductType.Validate("Groceries", null, out ProductType? productType);if (validationError is null)
{
logger.Information("Product type {Type} found with Validate", productType);
}
else
{
logger.Warning("Failed to fetch the product type with Validate. Validation error: {validationError}", validationError.ToString());
}------------
// implicit conversion to the type of the key
string key = ProductType.Groceries; // "Groceries"------------
// Equality comparison
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 (with "Action")
productType.Switch(groceries: () => logger.Information("Switch with Action: Groceries"),
housewares: () => logger.Information("Switch with Action: Housewares"));
// Switch-case with parameter (Action) to prevent closures
productType.Switch(logger,
groceries: static l => l.Information("Switch with Action: Groceries"),
housewares: static l => l.Information("Switch with Action: Housewares"));// Switch case returning a value (Func)
var returnValue = productType.Switch(groceries: static () => "Switch with Func: Groceries",
housewares: static () => "Switch with Func: Housewares");// Switch case with parameter returning a value (Func) to prevent closures
var returnValue = productType.Switch(logger,
groceries: static l => "Switch with Func: Groceries",
housewares: static l => "Switch with Func: Housewares");// Map an item to another instance
returnValue = productType.Map(groceries: "Map: Groceries",
housewares: "Map: Housewares");
------------// Implements IParsable which is especially helpful with minimal apis.
bool parsed = ProductType.TryParse("Groceries", null, out ProductType? parsedProductType);------------
// Implements IFormattable if the underlyng type (like int) is an IFormattable itself.
var formatted = ProductGroup.Fruits.ToString("000", CultureInfo.InvariantCulture); // 001------------
// Implements IComparable and IComparable if the key member type (like int) is an IComparable itself.
var comparison = ProductGroup.Fruits.CompareTo(ProductGroup.Vegetables); // -1// Implements comparison operators (<,<=,>,>=) if the underlyng type (like int) has comparison operators itself.
var isBigger = ProductGroup.Fruits > ProductGroup.Vegetables;
```Definition of a new Smart Enum with 1 custom property `RequiresFoodVendorLicense` and 1 method `Do` with different behaviors for different enum items.
```C#
[SmartEnum]
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 sealed 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](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects)
Features:
* Roslyn Analyzers and CodeFixes help the developers to implement the Value Objects correctly
* Choice between [Simple Value Objects](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#simple-value-objects) and [Complex Value Objects](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#complex-value-objects)
* Allows custom fields, properties and methods
* Provides appropriate factory methods for creation of new value objects based on the specified properties/fields
* [Factory methods can be renamed](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#rename-factory-methods)
* Allows custom [validation](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#validation) of [constructor](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#validation-of-the-constructor-arguments) and [factory method](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#validation-of-the-factory-method-arguments) arguments
* Allows [custom type to pass validation error(s)](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#custom-type-for-validation-errors)
* *[Simple Value Objects only]* Allows cast and type conversion from key-member type to Value Object and vice versa
* *[Simple Value Objects only]* Provides an implementation of `IFormattable` if the key member is an `IFormattable`
* Provides proper implementation of `Equals`, `GetHashCode`, `ToString` and equality comparison via `==` and `!=`
* Provides implementation of `IComparable`, `IComparable`, `IFormattable`, `IParsable` and comparison operators `<`, `<=`, `>`, `>=`
* [Allows custom equality comparison](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#custom-equality-comparer) and [custom comparer](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#custom-comparer)
* Configurable handling of [null](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#null-in-factory-methods-yields-null) and [empty strings](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#empty-string-in-factory-methods-yields-null)
* [JSON support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#json-serialization) (`System.Text.Json` and `Newtonsoft.Json`)
* [Support for Minimal Api Parameter Binding and ASP.NET Core Model Binding](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#support-for-minimal-api-parameter-binding-and-aspnet-core-model-binding)
* [Entity Framework Core support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#support-for-entity-framework-core) (`ValueConverter`)
* [MessagePack support](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#messagepack-serialization) (`IMessagePackFormatter`)
* [Logging for debugging or getting insights](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Value-Objects#logging-v610-or-higher)## Simple Value Object
A simple value object has 1 field/property only, i.e., it is kind of wrapper for another (primitive) type. The main use case is to prevent creation of values/instances which are considered invalid according to some rules.
In DDD (domain-driven design), working with primitive types, like string, directly is called primitive obsession and should be avoided.Most simple value objects with a key member of type `string` and another one (which is a struct) with an `int`.
```C#
[ValueObject]
public sealed partial class ProductName
{
}[ValueObject]
public readonly partial struct Amount
{
}
```After the implementation of a value object, a Roslyn source generator kicks in and implements the rest. Following API is available from now on.
```C#
// Factory method for creation of new instances.
// Throws ValidationException if the validation fails (if we had any)
ProductName apple = ProductName.Create("Apple");// Alternatively, using an explicit cast, which behaves the same way as calling "ProductName.Create"
ProductName apple = (ProductName)"Apple";-----------
// The same as above but returns a bool instead of throwing an exception (dictionary-style)
bool created = ProductName.TryCreate("Chocolate", out ProductName? chocolate);-----------
// Similar to TryCreate but returns a ValidationError instead of a boolean.
ValidationError? validationError = ProductName.Validate("Chocolate", null, out var chocolate);if (validationError is null)
{
logger.Information("Product name {Name} created", chocolate);
}
else
{
logger.Warning("Failed to create product name. Validation result: {validationError}", validationError.ToString());
}-----------
// Implicit conversion to the type of the key member
string valueOfTheProductName = apple; // "Apple"-----------
// Equality comparison compares the key member using default comparer by default.
// Key members of type `string` are compared with 'StringComparer.OrdinalIgnoreCase' by default.
bool equal = apple.Equals(apple);-----------
// Equality comparison operators: '==' and '!='
bool equal = apple == apple;
bool notEqual = apple != apple;-----------
// Hash code: combined hash code of type and key member.
// Strings are using 'StringComparer.OrdinalIgnoreCase' by default.
int hashCode = apple.GetHashCode();-----------
// 'ToString' implementation return the string representation of the key member
string value = apple.ToString(); // "Apple"------------
// Implements IParsable which is especially helpful with minimal apis.
bool success = ProductName.TryParse("New product name", null, out var productName);ProductName productName = ProductName.Parse("New product name", null);
------------
// Implements "IFormattable" if the key member is an "IFormattable".
Amount amount = Amount.Create(42);
string formattedValue = amount.ToString("000", CultureInfo.InvariantCulture); // "042"------------
// Implements "IComparable" if the key member is an "IComparable",
// or if custom comparer is provided.
Amount amount = Amount.Create(1);
Amount otherAmount = Amount.Create(2);int comparison = amount.CompareTo(otherAmount); // -1
------------
// Implements comparison operators (<,<=,>,>=) if the key member has comparison operators itself.
bool isBigger = amount > otherAmount;// Implements comparison operators to compare the value object with an instance of key-member-type,
// if "ComparisonOperators" is set "OperatorsGeneration.DefaultWithKeyTypeOverloads"
bool isBigger = amount > 2;------------
// Implements addition / subtraction / multiplication / division if the key member supports corresponding operators
Amount sum = amount + otherAmount;// Implements operators that accept an instance of key-member-type,
// if the "OperatorsGeneration" is set "DefaultWithKeyTypeOverloads"
Amount sum = amount + 2;------------
// Provides a static default value "Empty" (similar to "Guid.Empty"), if the value object is a struct
Amount defaultValue = Amount.Empty; // same as "Amount defaultValue = default;"
```## Complex Value Objects
A complex value object is an immutable `class` or a `readonly struct` with a `ComplexValueObjectAttribute`. Complex value object usually has multiple readonly fields/properties.
A simple example would be a `Boundary` with 2 properties. One property is the lower boundary and the other is the upper boundary. Yet again, we skip the validation at the moment.
```C#
[ComplexValueObject]
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:
```C#
// Factory method for creation of new instances.
// Throws ValidationException if the validation fails (if we had any)
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 ValidationError instead of a boolean.
ValidationError? validationError = Boundary.Validate(lower: 1, upper: 2, out Boundary? boundary);if (validationError is null)
{
logger.Information("Boundary {Boundary} created", boundary);
}
else
{
logger.Warning("Failed to create boundary. Validation result: {validationError}", validationError.ToString());
}-----------
// Equality comparison 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 }"
```# Discriminated Unions
Install: `Install-Package Thinktecture.Runtime.Extensions` (requires version 8.x.x)
Documentation: [Discriminated Unions](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/wiki/Discriminated-Unions)
There are 2 types of unions: `ad hoc union` and `"regular" unions`
## Ad hoc unions
Features:
* Roslyn Analyzers and CodeFixes help the developers to implement the unions correctly
* Provides proper implementation of `Equals`, `GetHashCode`, `ToString` and equality comparison via `==` and `!=`
* Switch-Case/Map
* Renaming of properties
* Definition of nullable reference typesDefinition of a basic union with 2 types using a `class`, a `struct` or `ref struct`:
```csharp
// class
[Union]
public partial class TextOrNumber;// struct
[Union]
public partial struct TextOrNumber;// ref struct
[Union]
public ref partial struct TextOrNumber;// Up to 5 types
[Union]
public partial class MyUnion;
```Behind the scenes a Roslyn Source Generator generates additional code. Some of the features that are now available are ...
```csharp
// Implicit conversion from one of the defined generics.
TextOrNumber textOrNumberFromString = "text";
TextOrNumber textOrNumberFromInt = 42;// Check the type of the value.
// By default, the properties are named using the name of the type (`String`, `Int32`)
bool isText = textOrNumberFromString.IsString;
bool isNumber = textOrNumberFromString.IsInt32;// Getting the typed value.
// Throws "InvalidOperationException" if the current value doesn't match the calling property.
// By default, the properties are named using the name of the type (`String`, `Int32`)
string text = textOrNumberFromString.AsString;
int number = textOrNumberFromInt.AsInt32;// Alternative approach is to use explicit cast.
// Behavior is identical to methods "As..."
string text = (string)textOrNumberFromString;
int number = (int)textOrNumberFromInt;// Getting the value as object, i.e. untyped.
object value = textOrNumberFromString.Value;// Implementation of Equals, GetHashCode and ToString
// PLEASE NOTE: Strings are compared using "StringComparison.OrdinalIgnoreCase" by default! (configurable)
bool equals = textOrNumberFromInt.Equals(textOrNumberFromString);
int hashCode = textOrNumberFromInt.GetHashCode();
string toString = textOrNumberFromInt.ToString();// Equality comparison operators
bool equal = textOrNumberFromInt == textOrNumberFromString;
bool notEqual = textOrNumberFromInt != textOrNumberFromString;
```There are multiple overloads of *switch-cases*: with `Action`, `Func` and concrete values.
To prevent *closures*, you can pass a value to method `Switch`, which is going to be passed to provided callback (`Action`/`Func`).By default, the names of the method arguments are named after the type specified by `UnionAttribute`.
Reserved C# keywords (like `string`) must string with `@` (like `@string`, `@default`, etc.).```csharp
// With "Action"
textOrNumberFromString.Switch(@string: s => logger.Information("[Switch] String Action: {Text}", s),
int32: i => logger.Information("[Switch] Int Action: {Number}", i));// With "Action". Logger is passed as additional parameter to prevent closures.
textOrNumberFromString.Switch(logger,
@string: static (l, s) => l.Information("[Switch] String Action with logger: {Text}", s),
int32: static (l, i) => l.Information("[Switch] Int Action with logger: {Number}", i));// With "Func"
var switchResponse = textOrNumberFromInt.Switch(@string: static s => $"[Switch] String Func: {s}",
int32: static i => $"[Switch] Int Func: {i}");// With "Func" and additional argument to prevent closures.
var switchResponseWithContext = textOrNumberFromInt.Switch(123.45,
@string: static (value, s) => $"[Switch] String Func with value: {ctx} | {s}",
int32: static (value, i) => $"[Switch] Int Func with value: {ctx} | {i}");// Use `Map` instead of `Switch` to return concrete values directly.
var mapResponse = textOrNumberFromString.Map(@string: "[Map] Mapped string",
int32: "[Map] Mapped int");
```Use `T1Name`/`T2Name` of the `UnionAttribute` to get more meaningful names.
```csharp
[Union(T1Name = "Text",
T2Name = "Number")]
public partial class TextOrNumber;
```The properties and method arguments are renamed accordingly:
```csharp
bool isText = textOrNumberFromString.IsText;
bool isNumber = textOrNumberFromString.IsNumber;string text = textOrNumberFromString.AsText;
int number = textOrNumberFromInt.AsNumber;textOrNumberFromString.Switch(text: s => logger.Information("[Switch] String Action: {Text}", s),
number: i => logger.Information("[Switch] Int Action: {Number}", i));
```## Regular unions
Features:
* Roslyn Analyzers and CodeFixes help the developers to implement the unions correctly
* Can be a `class` or `record`
* Switch-Case/Map
* Supports generics
* Derived types can be simple classes or something complex like a [value object](#value-objects).Simple union using a class and a value object:
```csharp
[Union]
public partial class Animal
{
[ValueObject]
public partial class Dog : Animal;public sealed class Cat : Animal;
}```
Similar example as above but with `records`:
```csharp
[Union]
public partial record AnimalRecord
{
public sealed record Dog(string Name) : AnimalRecord;public sealed record Cat(string Name) : AnimalRecord;
}
```A union type (i.e. the base class) with a property:
```csharp
[Union]
public partial class Animal
{
public string Name { get; }private Animal(string name)
{
Name = name;
}public sealed class Dog(string Name) : Animal(Name);
public sealed class Cat(string Name) : Animal(Name);
}
```A `record` with a generic:
```csharp
[Union]
public partial record Result
{
public record Success(T Value) : Result;public record Failure(string Error) : Result;
public static implicit operator Result(T value) => new Success(value);
public static implicit operator Result(string error) => new Failure(error);
}
```One of the main purposes for a regular union is their exhaustiveness, i.e. all member types are accounted for in a switch/map:
```csharp
Animal animal = new Animal.Dog("Milo");animal.Switch(dog: d => logger.Information("Dog: {Dog}", d),
cat: c => logger.Information("Cat: {Cat}", c));var result = animal.Map(dog: "Dog",
cat: "Cat");
```Use flags `SwitchMethods` and `MapMethods` for generation of `SwitchPartially`/`MapPartially`:
```csharp
[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads,
MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)]
public partial record AnimalRecord
{
public sealed record Dog(string Name) : AnimalRecord;public sealed record Cat(string Name) : AnimalRecord;
}---------------------------
Animal animal = new Animal.Dog("Milo");animal.SwitchPartially(@default: a => logger.Information("Default: {Animal}", a),
cat: c => logger.Information("Cat: {Cat}", c.Name));
```