Id
Aggregate requires you to specify identity type. Every aggregate has an Id property of the specified type. CoreLib supports three different flavors of IDs:
- Primitive types like
Guid,int, etc. - Generic type wrappers.
- Source generated IDs.
From CoreLib v8, Source Generated IDs are the default one, with primitive types being fallbacks if source generated one cannot be used.
Packages
| Package | Link | Application in section |
|---|---|---|
| LeanCode.DomainModels | IAggregateRoot |
|
| LeanCode.DomainModels.Generators | Ids |
Source generated IDs
Source generated IDs leverage Source Generators to generate fully functional ID structs that can work as IDs for entities. They are specialized for a particular entity type, but don't use the generic mechanisms of the language. They support the same feature set as aforementioned IDs.
How to use source-generated IDs
To use the source-generated IDs, you first need to reference the source generator that does the heavy lifting:
<PackageReference Include="LeanCode.DomainModels.Generators" Version="(version)" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
Then, you need to add a partial struct record that will be filled up by the compiler when building the project, and decorate it with TypedIdAttribute:
[TypedId(TypedIdFormat.RawInt)]
public readonly partial record struct EmployeeId;
Then, you can use it as an aggregate id and across the project.
public class Employee : IAggregateRoot<EmployeeId>
{
public EmployeeId Id { get; }
// . . .
}
API
The generated ID supports the following operations (example for RawGuid):
public readonly partial record struct ID : IEquatable<ID>,
IComparable<ID>,
ISpanFormattable,
IUtf8SpanFormattable,
ISpanParsable<ID>,
IEqualityOperators<ID, ID, bool>
{
public static readonly ID Empty;
public Guid Value { get; }
public bool IsEmpty { get; }
// Parsing from backing type (if the ID is just a wrapper for raw backing type)
public static ID Parse(Guid v);
public static ID? ParseNullable(Guid? id);
public static bool TryParse([NotNullWhen(true)] Guid? v, out ID id);
public static bool IsValid([NotNullWhen(true)] Guid? v);
public static ID New(); // Only if generation is possible
}
Prefixed ID features
Prefixed IDs (PrefixedGuid, PrefixedUlid, PrefixedString) provide additional APIs for accessing components:
// PrefixedGuid
public Guid Guid { get; }
public (string prefix, Guid data) Destructure();
// PrefixedUlid
public Ulid Ulid { get; }
public (string prefix, Ulid data) Destructure();
// PrefixedString
public string ValuePart { get; }
public (string prefix, string data) Destructure();
public static ID FromValuePart(string valuePart);
Example usage:
var id = OrderId.Parse("order_01ARZ3NDEKTSV4RRFFQ69G5FAV");
var (prefix, ulid) = id.Destructure(); // ("order", Ulid)
var rawUlid = id.Ulid; // Access raw Ulid directly
Configuration
The format of the ID can be configured using:
TypedIdFormat- this configures the underlying type and the format of the ID. You need to specify it as a first parameter to theTypedIdAttribute. Possibilities:RawInt- usesintas the underlying type; works as a wrapper overint; does not support generating new IDs at runtime by default.RawLong- useslongas the underlying type; works as a wrapper overlong; does not support generating new IDs at runtime by default.RawGuid- usesGuidas the underlying type; works as a wrapper overGuid; can generate new ID at runtime usingGuid.NewGuid.RawString- usesstringas the underlying type; works as a wrapper over arbitrary strings; does not support generating new IDs at runtime.PrefixedGuid- usesstringas the underlying type; it is represented as a(prefix)_(guid)string that can be generated at runtime; by default(prefix)is a lowercase class name withIdsuffix removed.PrefixedUlid- usesstringas the underlying type; it is represented as a(prefix)_(ulid)string that can be generated at runtime; by default(prefix)is a lowercase class name withIdsuffix removed.PrefixedString- usesstringas the underlying type; it is represented as a(prefix)_(value)string where(value)is an arbitrary string; does not support generating new IDs at runtime.
CustomPrefix- forPrefixed*formats, you can configure what prefix it uses (if you e.g. want to use a shorter one).SkipRandomGenerator- setting this totruewill skip generatingNewfactory method (for formats that support generation).MaxValueLength- required maximum length constraint for the value part of the string IDs. ForRawString, this is the entire string length. ForPrefixedString, this excludes the prefix and separator. Validation is performed inParse/IsValidmethods. Used for configuring columns in the database, so consider SQL Server's 900-byte key limit (~450 nvarchar chars) when choosing this value.
Examples:
[TypedId(TypedIdFormat.PrefixedGuid, CustomPrefix = "employee")]
public readonly partial record struct VeryLongEmployeeId;
// The `VeryLongEmployeeId` will have format `employee_(guid)`, with `New` using `Guid.NewGuid` as random source.
[TypedId(TypedIdFormat.RawString, MaxValueLength = 100)]
public readonly partial record struct ExternalId;
// The `ExternalId` wraps any string up to 100 characters. Exposes `MaxLength` static property.
[TypedId(TypedIdFormat.PrefixedString, CustomPrefix = "ext", MaxValueLength = 50)]
public readonly partial record struct ExternalRefId;
// The `ExternalRefId` has format `ext_(value)` where value can be up to 50 characters.
// Exposes `MaxValueLength` (50) and `MaxLength` (54 = 3 + 1 + 50) static properties.
Generic type wrappers
The domain part of the library supports a set of generic IDs:
All the types give you type safety when passing the IDs, without introducing penalty (they basically work as newtypes). Unfortunately, they require an entity to be defined beforehand - it works as a generic parameter. This means you can't use it without a corresponding entity type. This poses a problem if you want to use the ID outside the parent domain. It is also quite hard to use - you need to know the exact ID format before you reference it (you need to choose between the four types when you just want to reference other entity).