Skip to content

dombrovsky/StrongTypeIdGenerator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

StrongTypeIdGenerator

Source generator that helps you create strongly-typed identifiers in your C# projects. It supports Guid, string-based, and combined identifiers.

NuGet License: MIT


Getting Started

Define your ID type:

[StringId]
partial class FooId
{
}

The generator will produce:

[System.ComponentModel.TypeConverter(typeof(FooIdConverter))]
partial class FooId : ITypedIdentifier<FooId, string>
{
    public FooId(string value)
    {
        if (value is null)
        {
            throw new ArgumentNullException(nameof(value));
        }

        Value = value;
    }

    public static FooId Unspecified { get; } = new FooId(string.Empty);

    public string Value { get; }

    [return: NotNullIfNotNull(nameof(value))]
    public static implicit operator FooId?(string? value) { ... }

    [return: NotNullIfNotNull(nameof(value))]
    public static implicit operator string?(FooId? value) { ... }

    public bool Equals(FooId? other) { ... }

    public int CompareTo(FooId? other) { ... }

    public override bool Equals(object? obj) { ... }

    public override int GetHashCode() => Value.GetHashCode();

    public override string ToString() => Value;

    public string ToString(string? format, IFormatProvider? formatProvider) => Value;

    public static bool operator ==(FooId left, FooId right) { ... }

    public static bool operator !=(FooId left, FooId right) { ... }

    public static bool operator <(FooId left, FooId right) { ... }

    public static bool operator <=(FooId left, FooId right) { ... }

    public static bool operator >(FooId left, FooId right) { ... }

    public static bool operator >=(FooId left, FooId right) { ... }

    private sealed partial class FooIdConverter : TypeToStringConverter<FooId>
    {
        protected override string? InternalConvertToString(FooId value)
        {
            return value.Value;
        }

        protected override FooId? InternalConvertFromString(string value)
        {
            return new FooId(value);
        }
    }
}

Design decisions

There are a few opinionated principles regarding what strong type identifiers should and should not do, which may be different from similar libraries and are reasons this project existence.

Idenifier type should be a reference type, not a value type.

Being able to protect invariants and not allow instance of id with invalid value to exist, is chosen over avoiding additional object allocation.

Ability to define custom id precondition checks.

If Id class defines method static string CheckValue(string value), that method would be called from generated constructor.

[StringId]
partial class FooId
{
    private static string CheckValue(string value)
    {
        if (value.Length > 10)
        {
            throw new ArgumentException("Value is too long", nameof(value));
        }

        return value;
    }
}

No dependency on serialization libraries.

StrongTypeIdGenerator only defines System.ComponentModel.TypeConverter that can convert to and from string. No System.Text.Json or Newtonsoft.Json or EF Core converters defined.

This way Id types can be defined in netstandard2.0 libraries with no additional dependencies.

The proposed way to use generated Id classes in serialization e.g. with System.Text.Json is to provide custom JsonConverterFactory to serializer, that would utilize generated TypeConverter.

Usage

Define ID types easily:

[StringId]
public sealed partial class FooId
{
}

[GuidId]
public sealed partial class BarId
{
}

Or use [CombinedId] to create a composite identifier:

[CombinedId(typeof(BarId), "BarId", typeof(string), "StringId", typeof(Guid), "GuidId", typeof(int), "IntId")]
public partial class FooBarCombinedId
{
}

Combined indentifier supports other StrongTypeId generated types and primitives e.g. string, int, Guid.

Custom Validation

You can add custom validation logic to your ID types by defining a CheckValue method. The method will be called from the constructor and can validate (throw exceptions) or modify the input value.

String and Guid IDs

For StringId and GuidId, define a method with this signature:

private static string CheckValue(string value)
private static Guid CheckValue(Guid value)
{
    // Validation logic here
    return value;
}

Combined IDs

For CombinedId, the CheckValue method should accept individual parameters matching the constructor and return a tuple with the validated values:

private static (BarId, string, Guid, int) CheckValue(BarId barId, string stringId, Guid guidId, int intId)
{
    // Validation logic here
    return (barId, stringId, guidId, intId);
}

The CheckValue method is called from the constructor and its result is used to set the properties of the ID class.

Custom Value Property Name

You can customize the name of the property that holds the identifier's value by setting the ValuePropertyName property on the attribute:

[GuidId(ValuePropertyName = "Uuid")]
public sealed partial class BarId
{
}

And generated class fill get 'Uuid' property instead of 'Value':

public sealed partial class BarId
{
  ...
  public Guid Uuid { get; }
  ...
}

When using a custom property name, the generated class will still implement the ITypedIdentifier<T> interface by providing an explicit implementation for the Value property that forwards to your custom property.

Private Constructor Generation

You can generate private constructors for your ID types to enforce controlled instantiation through factory methods. This is useful for implementing business rules or ensuring specific creation patterns.

Set the GenerateConstructorPrivate property to true on any of the ID attributes:

String and Guid IDs

[StringId(GenerateConstructorPrivate = true)]
public partial class SecureToken
{
    public static SecureToken CreateNew()
    {
        return new SecureToken(GenerateSecureRandomString());
    }

    public static SecureToken FromExisting(string value)
    {
        // Validate the token format
        if (!IsValidTokenFormat(value))
            throw new ArgumentException("Invalid token format");
            
        return new SecureToken(value);
    }

    private static string GenerateSecureRandomString() => /* implementation */;
    private static bool IsValidTokenFormat(string value) => /* implementation */;
}

[GuidId(GenerateConstructorPrivate = true)]
public partial class UserId
{
    public static UserId CreateNew() => new UserId(Guid.NewGuid());
    
    public static UserId FromString(string value) => new UserId(Guid.Parse(value));
}

Combined IDs

[CombinedId(typeof(Guid), "TenantId", typeof(string), "UserId", GenerateConstructorPrivate = true)]
public partial class CompositeKey
{
    public static CompositeKey CreateForTenant(Guid tenantId, string userId)
    {
        if (tenantId == Guid.Empty)
            throw new ArgumentException("Tenant ID cannot be empty");
        if (string.IsNullOrWhiteSpace(userId))
            throw new ArgumentException("User ID cannot be empty");
            
        return new CompositeKey(tenantId, userId);
    }
}

Note: When using private constructors:

  • Implicit conversion operators still work and call the private constructor
  • The Unspecified static property is still accessible
  • TypeConverter functionality continues to work for serialization scenarios
  • Custom CheckValue methods are still called during construction

Acknowledgements

Inspired by a great library https://github.com/andrewlock/StronglyTypedId.

About

Simple unambitious strongly typed identifier generator

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages