https://github.com/Cysharp/UnitGenerator
C# Source Generator to create value-object, inspired by units of measure.
https://github.com/Cysharp/UnitGenerator
Last synced: 21 days ago
JSON representation
C# Source Generator to create value-object, inspired by units of measure.
- Host: GitHub
- URL: https://github.com/Cysharp/UnitGenerator
- Owner: Cysharp
- License: mit
- Created: 2020-12-10T00:10:19.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2025-03-19T06:46:10.000Z (about 2 months ago)
- Last Synced: 2025-04-14T23:17:02.947Z (24 days ago)
- Language: C#
- Homepage:
- Size: 184 KB
- Stars: 363
- Watchers: 10
- Forks: 20
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- RSCG_Examples - https://github.com/Cysharp/UnitGenerator
README
UnitGenerator
===
[](https://github.com/Cysharp/UnitGenerator/actions) [](https://github.com/Cysharp/UnitGenerator/releases)C# Source Generator to create [Value object](https://en.wikipedia.org/wiki/Value_object) pattern, also inspired by [units of measure](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/units-of-measure) to support all arithmetic operators and serialization.
NuGet: [UnitGenerator](https://www.nuget.org/packages/UnitGenerator)
```
Install-Package UnitGenerator
```Execute in Unity Game Engine is also supported, please see the [Unity](#use-for-unity) section for details.
## Introduction
For example, Identifier, UserId is comparable only to UserId, and cannot be assigned to any other type. Also, arithmetic operations are not allowed.
```csharp
using UnitGenerator;[UnitOf(typeof(int))]
public readonly partial struct UserId; { }
```or when using C#11 and NET7 you can use
```csharp
using UnitGenerator;[UnitOf] public readonly partial struct UserId;
```will generates
```csharp
[System.ComponentModel.TypeConverter(typeof(UserIdTypeConverter))]
public readonly partial struct UserId : IEquatable
{
readonly int value;
public UserId(int value)
{
this.value = value;
}public readonly int AsPrimitive() => value;
public static explicit operator int(UserId value) => value.value;
public static explicit operator UserId(int value) => new UserId(value);
public bool Equals(UserId other) => value.Equals(other.value);
public override bool Equals(object? obj) => // snip...
public override int GetHashCode() => value.GetHashCode();
public override string ToString() => value.ToString();
public static bool operator ==(in UserId x, in UserId y) => x.value.Equals(y.value);
public static bool operator !=(in UserId x, in UserId y) => !x.value.Equals(y.value);private class UserIdTypeConverter : System.ComponentModel.TypeConverter
{
// snip...
}
}
```However, Hp in games, should not be allowed to be assigned to other types, but should support arithmetic operations with int. For example double heal = `target.Hp = Hp.Min(target.Hp * 2, target.MaxHp)`.
```csharp
[UnitOf(UnitGenerateOptions.ArithmeticOperator | UnitGenerateOptions.ValueArithmeticOperator | UnitGenerateOptions.Comparable | UnitGenerateOptions.MinMaxMethod)]
public readonly partial struct Hp;// -- generates
[System.ComponentModel.TypeConverter(typeof(HpTypeConverter))]
public readonly partial struct Hp
: IEquatable
#if NET7_0_OR_GREATER
, IEqualityOperators
#endif
, IComparable
#if NET7_0_OR_GREATER
, IComparisonOperators
#endif
#if NET7_0_OR_GREATER
, IAdditionOperators
, ISubtractionOperators
, IMultiplyOperators
, IDivisionOperators
, IUnaryPlusOperators
, IUnaryNegationOperators
, IIncrementOperators
, IDecrementOperators
#endif
{
readonly int value;public Hp(int value)
{
this.value = value;
}public int AsPrimitive() => value;
public static explicit operator int(Hp value) => value.value;
public static explicit operator Hp(int value) => new Hp(value);
public bool Equals(Hp other) => value.Equals(other.value);
public override bool Equals(object? obj) => // snip...
public override int GetHashCode() => value.GetHashCode();
public override string ToString() => value.ToString();
public static bool operator ==(in Hp x, in Hp y) => x.value.Equals(y.value);
public static bool operator !=(in Hp x, in Hp y) => !x.value.Equals(y.value);
private class HpTypeConverter : System.ComponentModel.TypeConverter { /* snip... */ }// UnitGenerateOptions.ArithmeticOperator
public static Hp operator +(Hp x, Hp y) => new Hp(checked((int)(x.value + y.value)));
public static Hp operator -(Hp x, Hp y) => new Hp(checked((int)(x.value - y.value)));
public static Hp operator *(Hp x, Hp y) => new Hp(checked((int)(x.value * y.value)));
public static Hp operator /(Hp x, Hp y) => new Hp(checked((int)(x.value / y.value)));
public static Hp operator ++(Hp x) => new Hp(checked((int)(x.value + 1)));
public static Hp operator --(Hp x) => new Hp(checked((int)(x.value - 1)));
public static Hp operator +(A value) => new((int)(+value.value));
public static Hp operator -(A value) => new((int)(-value.value));// UnitGenerateOptions.ValueArithmeticOperator
public static Hp operator +(Hp x, in int y) => new Hp(checked((int)(x.value + y)));
public static Hp operator -(Hp x, in int y) => new Hp(checked((int)(x.value - y)));
public static Hp operator *(Hp x, in int y) => new Hp(checked((int)(x.value * y)));
public static Hp operator /(Hp x, in int y) => new Hp(checked((int)(x.value / y)));// UnitGenerateOptions.Comparable
public int CompareTo(Hp other) => value.CompareTo(other.value);
public static bool operator >(Hp x, Hp y) => x.value > y.value;
public static bool operator <(Hp x, Hp y) => x.value < y.value;
public static bool operator >=(Hp x, Hp y) => x.value >= y.value;
public static bool operator <=(Hp x, Hp y) => x.value <= y.value;// UnitGenerateOptions.MinMaxMethod
public static Hp Min(Hp x, Hp y) => new Hp(Math.Min(x.value, y.value));
public static Hp Max(Hp x, Hp y) => new Hp(Math.Max(x.value, y.value));
}
```You can configure with `UnitGenerateOptions`, which method to implement.
```csharp
[Flags]
enum UnitGenerateOptions
{
None = 0,
ImplicitOperator = 1,
ParseMethod = 1 << 1,
MinMaxMethod = 1 << 2,
ArithmeticOperator = 1 << 3,
ValueArithmeticOperator = 1 << 4,
Comparable = 1 << 5,
Validate = 1 << 6,
JsonConverter = 1 << 7,
MessagePackFormatter = 1 << 8,
DapperTypeHandler = 1 << 9,
EntityFrameworkValueConverter = 1 << 10,
WithoutComparisonOperator = 1 << 11,
JsonConverterDictionaryKeySupport = 1 << 12,
Normalize = 1 << 13,
}
```UnitGenerateOptions has some serializer support. For example, a result like `Serialize(userId) => { Value = 1111 }` is awful. The value-object should be serialized natively, i.e. `Serialize(useId) => 1111`, and should be able to be added directly to a database, etc.
Currently UnitGenerator supports [MessagePack for C#](https://github.com/MessagePack-CSharp/MessagePack-CSharp), System.Text.Json(JsonSerializer), [Dapper](https://github.com/StackExchange/Dapper) and EntityFrameworkCore.
```csharp
[UnitOf(UnitGenerateOptions.MessagePackFormatter)]
public readonly partial struct UserId;// -- generates
[MessagePackFormatter(typeof(UserIdMessagePackFormatter))]
public readonly partial struct UserId
{
class UserIdMessagePackFormatter : IMessagePackFormatter
{
public void Serialize(ref MessagePackWriter writer, UserId value, MessagePackSerializerOptions options)
{
options.Resolver.GetFormatterWithVerify().Serialize(ref writer, value.value, options);
}public UserId Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
return new UserId(options.Resolver.GetFormatterWithVerify().Deserialize(ref reader, options));
}
}
}
```## Table of Contents
- [UnitOfAttribute](#unitofattribute)
- [UnitGenerateOptions](#unitgenerateoptions)
- [ImplicitOperator](#implicitoperator)
- [ParseMethod](#parsemethod)
- [MinMaxMethod](#minmaxmethod)
- [ArithmeticOperator](#arithmeticoperator)
- [ValueArithmeticOperator](#valuearithmeticoperator)
- [Comparable](#comparable)
- [WithoutComparisonOperator](#withoutcomparisonoperator)
- [Validate](#validate)
- [Normalize](#normalize)
- [JsonConverter](#jsonconverter)
- [JsonConverterDictionaryKeySupport](#jsonconverterdictionarykeysupport)
- [MessagePackFormatter](#messagepackformatter)
- [DapperTypeHandler](#dappertypehandler)
- [EntityFrameworkValueConverter](#entityframeworkvalueconverter)
- [Use for Unity](#use-for-unity)
- [License](#license)## UnitOfAttribute
When referring to the UnitGenerator, it generates a internal `UnitOfAttribute`.```csharp
namespace UnitGenerator
{
[AttributeUsage(AttributeTargets.Struct, AllowMultiple = false)]
internal class UnitOfAttribute : Attribute
{
public Type Type { get; }
public UnitGenerateOptions Options { get; }
public UnitArithmeticOperators ArithmeticOperators { get; set; }
public string? ToStringFormat { get; set; }
public UnitOfAttribute(Type type, UnitGenerateOptions options = UnitGenerateOptions.None) { ... }
}#if NET7_0_OR_GREATER
[AttributeUsage(AttributeTargets.Struct, AllowMultiple = false)]
internal class UnitOfAttribute : Attribute
{
public Type Type { get; }
public UnitGenerateOptions Options { get; }
public UnitArithmeticOperators ArithmeticOperators { get; set; } = UnitArithmeticOperators.All;
public string? ToStringFormat { get; set; }public UnitOfAttribute(UnitGenerateOptions options = UnitGenerateOptions.None)
{
this.Type = typeof(T);
this.Options = options;
}
}
#endif
}
```You can attach this attribute with any specified underlying type to `readonly partial struct`.
```csharp
[UnitOf(typeof(Guid))]
public readonly partial struct GroupId { }[UnitOf(typeof(string))]
public readonly partial struct Message { }[UnitOf(typeof(long))]
public readonly partial struct Power { }[UnitOf(typeof(byte[]))]
public readonly partial struct Image { }[UnitOf(typeof(DateTime))]
public readonly partial struct StartDate { }[UnitOf(typeof((string street, string city)))]
public readonly partial struct StreetAddress { }
```Standard UnitOf(`UnitGenerateOptions.None`) generates value constructor, `explicit operator`, `implement IEquatable`, `override GetHashCode`, `override ToString`, `==` and `!=` operator, `TypeConverter` for ASP.NET Core binding, `AsPrimitive` method.
If you want to retrieve primitive value, use `AsPrimitive()` instead of `.Value`. This is intended to avoid casual getting of primitive values (using the arithmetic operator option if available).
> When type is bool, also implements `true`, `false`, `!` operators.
```csharp
public static bool operator true(Foo x) => x.value;
public static bool operator false(Foo x) => !x.value;
public static bool operator !(Foo x) => !x.value;
```> When type is Guid or [Ulid](https://github.com/Cysharp/Ulid), also implements `New()` and `New***()` static operator.
> For Guid type in .NET 9.0 or later, these methods accept an optional `uuidV7` parameter. When `uuidV7` is set to `true`, the methods use `Guid.CreateVersion7()` internally.```csharp
public static GroupId New();
public static GroupId NewGroupId();
// overload .NET 9.0+
public static GroupId New(bool uuidV7);
public static GroupId NewGroupId(bool uuidV7);
```Second parameter `UnitGenerateOptions options` can configure which method to implement, default is `None`.
Optional named parameter: `ArithmeticOperators` can configure which generates operators specifically. Default is `Number`. (This can be used if UnitGenerateOptions.ArithmeticOperator is specified.)
Optional named parameter: `ToStringFormat` can configure `ToString` format. Default is null and output as $`{0}`.
## UnitGenerateOptions
When referring to the UnitGenerator, it generates a internal `UnitGenerateOptions` that is bit flag of which method to implement.
```csharp
[Flags]
internal enum UnitGenerateOptions
{
None = 0,
ImplicitOperator = 1,
ParseMethod = 2,
MinMaxMethod = 4,
ArithmeticOperator = 8,
ValueArithmeticOperator = 16,
Comparable = 32,
Validate = 64,
JsonConverter = 128,
MessagePackFormatter = 256,
DapperTypeHandler = 512,
EntityFrameworkValueConverter = 1024,
}
```You can use this with `[UnitOf]`.
```csharp
[UnitOf(typeof(int), UnitGenerateOptions.ArithmeticOperator | UnitGenerateOptions.ValueArithmeticOperator | UnitGenerateOptions.Comparable | UnitGenerateOptions.MinMaxMethod)]
public readonly partial struct Strength { }[UnitOf(typeof(DateTime), UnitGenerateOptions.Validate | UnitGenerateOptions.ParseMethod | UnitGenerateOptions.Comparable)]
public readonly partial struct EndDate { }[UnitOf(typeof(double), UnitGenerateOptions.ParseMethod | UnitGenerateOptions.MinMaxMethod | UnitGenerateOptions.ArithmeticOperator | UnitGenerateOptions.ValueArithmeticOperator | UnitGenerateOptions.Comparable | UnitGenerateOptions.Validate | UnitGenerateOptions.JsonConverter | UnitGenerateOptions.MessagePackFormatter | UnitGenerateOptions.DapperTypeHandler | UnitGenerateOptions.EntityFrameworkValueConverter)]
public readonly partial struct AllOptionsStruct { }
```You can setup project default options like this.
```csharp
internal static class UnitOfOptions
{
public const UnitGenerateOptions Default = UnitGenerateOptions.ArithmeticOperator | UnitGenerateOptions.ValueArithmeticOperator | UnitGenerateOptions.Comparable | UnitGenerateOptions.MinMaxMethod;
}[UnitOf(typeof(int), UnitOfOptions.Default)]
public readonly partial struct Hp { }
```### ImplicitOperator
```csharp
// Default
public static explicit operator U(T value) => value.value;
public static explicit operator T(U value) => new T(value);// UnitGenerateOptions.ImplicitOperator
public static implicit operator U(T value) => value.value;
public static implicit operator T(U value) => new T(value);
```### ParseMethod
```csharp
public static T Parse(string s)
public static bool TryParse(string s, out T result)
```### MinMaxMethod
```csharp
public static T Min(T x, T y)
public static T Max(T x, T y)
```### ArithmeticOperator
```csharp
public static T operator +(in T x, in T y) => new T(checked((U)(x.value + y.value)));
public static T operator -(in T x, in T y) => new T(checked((U)(x.value - y.value)));
public static T operator *(in T x, in T y) => new T(checked((U)(x.value * y.value)));
public static T operator /(in T x, in T y) => new T(checked((U)(x.value / y.value)));
public static T operator +(T value) => new((U)(+value.value));
public static T operator -(T value) => new((U)(-value.value));
public static T operator ++(T x) => new T(checked((U)(x.value + 1)));
public static T operator --(T x) => new T(checked((U)(x.value - 1)));
```In addition, all members conforming to [System.Numerics.INumber](https://learn.microsoft.com/ja-jp/dotnet/api/system.numerics.inumber-1) are generated.
If you want to suppress this and generate only certain operators, you can use the the `ArithmeticOperatros` option of `[UnitOf]` attribute as follows:
```csharp
[UnitOf(
typeof(int),
UnitGenerateOptions.ArithmeticOperator,
ArithmeticOperators = UnitArithmeticOperators.Addition | UnitArithmeticOperators.Subtraction)]
public readonly partial struct Hp { }
```| Value | Generates |
|-------------------------------------|----------------------------------------------------------------------------------------|
| UnitArithmeticOperators.Addition | `T operator +(T, T)` |
| UnitArithmeticOperators.Subtraction | `T operator -(T, T)` |
| UnitArithmeticOperators.Multiply | `T operator *(T, T)`, `T operator +(T)`, `T operator-(T)` |
| UnitArithmeticOperators.Division | `T operator /(T, T)`, `T operator +(T)`, `T operator-(T)` |
| UnitArithmeticOperators.Increment | `T operator ++(T)` |
| UnitArithmeticOperators.Decrement | `T operator --(T)` |### ValueArithmeticOperator
```csharp
public static T operator +(in T x, in U y) => new T(checked((U)(x.value + y)));
public static T operator -(in T x, in U y) => new T(checked((U)(x.value - y)));
public static T operator *(in T x, in U y) => new T(checked((U)(x.value * y)));
public static T operator /(in T x, in U y) => new T(checked((U)(x.value / y)));
```### Comparable
Implements `IComparable` and `>`, `<`, `>=`, `<=` operators.
```csharp
public U CompareTo(T other) => value.CompareTo(other.value);
public static bool operator >(in T x, in T y) => x.value > y.value;
public static bool operator <(in T x, in T y) => x.value < y.value;
public static bool operator >=(in T x, in T y) => x.value >= y.value;
public static bool operator <=(in T x, in T y) => x.value <= y.value;
```### WithoutComparisonOperator
Without implements `>`, `<`, `>=`, `<=` operators. For example, useful for Guid.
```csharp
[UnitOf(typeof(Guid), UnitGenerateOptions.Comparable | UnitGenerateOptions.WithoutComparisonOperator)]
public readonly partial struct FooId { }
```### Validate
Implements `partial void Validate()` method that is called on constructor.
```csharp
// You can implement this custom validate method.
[UnitOf(typeof(int), UnitGenerateOptions.Validate)]
public readonly partial struct SampleValidate
{
// impl here.
private partial void Validate()
{
if (value > 9999) throw new Exception("Invalid value range: " + value);
}
}// Source generator generate this codes.
public T(int value)
{
this.value = value;
this.Validate();
}
private partial void Validate();
```### Normalize
Implements `partial void Normalize(ref T value)` method that is called on constructor.
```csharp
// You can implement this custom normalize method to change value during initialization
[UnitOf(typeof(int), UnitGenerateOptions.Normalize)]
public readonly partial struct SampleValidate
{
// impl here.
private partial void Normalize(ref int value)
{
value = Math.Max(value, 9999);
}
}// Source generator generate this codes.
public T(int value)
{
this.value = value;
this.Normalize(ref this.value);
}
private partial void Normalize(ref int value);
```### JsonConverter
Implements `System.Text.Json`'s `JsonConverter`. It will be used `JsonSerializer` automatically.
```csharp
[JsonConverter(typeof(UserIdJsonConverter))]
public readonly partial struct UserId
{
class UserIdJsonConverter : JsonConverter
}
```### JsonConverterDictionaryKeySupport
Implements `JsonConverter`'s `WriteAsPropertyName/ReadAsPropertyName`. It supports from .NET 6, supports Dictionary's Key.
```csharp
var dict = Dictionary
JsonSerializer.Serialize(dict);
````### MessagePackFormatter
Implements MessagePack for C#'s `MessagePackFormatter`. It will be used `MessagePackSerializer` automatically.
```csharp
[MessagePackFormatter(typeof(UserIdMessagePackFormatter))]
public readonly partial struct UserId
{
class UserIdMessagePackFormatter : IMessagePackFormatter
}
```### DapperTypeHandler
Implements Dapper's TypeHandler by public accessibility. TypeHandler is automatically registered at the time of Module initialization.
```csharp
public readonly partial struct UserId
{
public class UserIdTypeHandler : Dapper.SqlMapper.TypeHandler
}[ModuleInitializer]
public static void AddTypeHandler()
{
Dapper.SqlMapper.AddTypeHandler(new A.ATypeHandler());
}
```### EntityFrameworkValueConverter
Implements EntityFrameworkCore's ValueConverter by public accessibility. It is not registered automatically so you need to register manually.
```csharp
public readonly partial struct UserId
{
public class UserIdValueConverter : ValueConverter
}// setup handler manually
builder.HasConversion(new UserId.UserIdValueConverter());
```## Use for Unity
Minimum supported Unity version is `2022.3.12f1`.
The easiest way is to install `UnitGenerator` from NuGet using [NuGetForUnity](https://github.com/GlitchEnzo/NuGetForUnity).
Alternatively, you can download `UnitGenerator.dll` from the [releases page](https://github.com/Cysharp/UnitGenerator/releases) page and set it up as a `RoslynAnalyzer` to make it work.
License
---
This library is under the MIT License.