Source generator that helps you create strongly-typed identifiers in your C# projects. It supports Guid, string-based, and combined identifiers.
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);
}
}
}
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.
Being able to protect invariants and not allow instance of id with invalid value to exist, is chosen over avoiding additional object allocation.
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;
}
}
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
.
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
.
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.
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;
}
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.
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.
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:
[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));
}
[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
Inspired by a great library https://github.com/andrewlock/StronglyTypedId.