Skip to content

Automatic implemention of stronly typed IDs via record structs with compile-time feature auto-detection

License

Notifications You must be signed in to change notification settings

devlooped/StructId

Repository files navigation

Icon StructId

Version Downloads License Build

An opinionated strongly-typed ID library that uses readonly record struct in C# for maximum performance, minimal memory allocation typed identifiers.

public readonly partial record struct UserId : IStructId<Guid>;

Unlike other such libraries for .NET, StructId introduces several unique features:

  1. Zero run-time dependencies: everything is source-generated in your project.
  2. Zero configuration: additional features are automatically added as you reference dependencies that require them. For example: if your project references EF Core, Dapper, or Newtonsoft.Json, the corresponding serialization and deserialization code will be emitted without any additional configuration for the generation itself.
  3. Leverages newest language and runtime features for cleaner and more efficient code, such as:
    1. IParsable<T>/ISpanParsable<T> for parsing from strings.
    2. Static interface members, for consistent TSelf.New(TId value) factory method and proper type constraint (via a provided INewable<TSelf, TId> interface).
    3. File-scoped C# templates for unparalelled authoring and extensibility experience.

Usage

After installing the StructId package, the project (with a direct reference to the StructId package) will contain the main interfaces IStruct (for string-typed IDs) and IStructId<TId>.

NOTE: the package only needs to be installed in the top-level project in your solution, since analyzers/generators will automatically propagate to referencing projects.

The package is a development dependency, meaning it will not add any run-time dependencies to your project (or package if you publish one that uses struct ids).

The default target namespace for the included types will match the RootNamespace of the project, but can be customized by setting the StructIdNamespace property.

You can simply declare a new ID type by implementing IStructId<TId>:

public readonly partial record struct UserId : IStructId<Guid>;

If the declaration is missing partial, readonly or record struct, a codefix will be offered to correct it.

codefix

The relevant constructor and Value property will be generated for you, as well as as a few other common interfaces, such as IComparable<T>, IParsable<TSelf>, etc.

If you want to customize the primary constructor (i.e. to add custom attributes), you can provide it yourself too:

public readonly partial record struct ProductId(int Value) : IStructId<int>;

It must contain a single parameter named Value (and codefixes will offer to rename or remove it if you don't need it anymore).

EF Core

If you are using EF Core, the package will automatically generate the necessary value converters, as well as an UseStructId extension method for DbContextOptionsBuilder to set them up:

var options = new DbContextOptionsBuilder<Context>()
    .UseSqlite("Data Source=ef.db")
    .UseStructId()
    .Options;

using var context = new Context(options);
// access your entities using struct ids

Alternatively, you can also invoke that method in the OnConfiguring method of your context:

protected override void OnConfiguring(DbContextOptionsBuilder builder) => builder.UseStructId();

Dapper

If you are using Dapper, the package will automatically generate required SqlMapper.TypeHandler<T> for your ID types. The UseStructId extension method for IDbConnection can be used to register them as needed:

using var connection = new SqliteConnection("Data Source=sqlite.db")

connection.UseStructId();
connection.Open();

The value types Guid, int, long and string have built-in support, as well as any other types that implement IParsable<T> and IFormattable (by persisting them as strings). This means that you can, for example, use Ulid out of the box without any further configuration or customization (since it implements both interfaces).

Customization via Templates

Virtually all the built-in interfaces and implementations are generated using the same compiled templates mechanism available to you. Templates are regular C# files in your project with a few constraints. Here's an example from the built-in ones:

using System;
using StructId;

[TStructId]
file partial record struct TSelf(IUtf8SpanFormattable Value) : IUtf8SpanFormattable
{
    /// <inheritdoc/>
    public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format, IFormatProvider? provider) 
        => ((IUtf8SpanFormattable)Value).TryFormat(utf8Destination, out bytesWritten, format, provider);
}

This type is considered a template because it's marked with the [TStructId] attribute. This introduces some restrictions that are enfored by an analyzer:

  1. The type must be a partial record struct since it will complement a partial declaration of that type by the user (i.e. partial record struct PersonId : IStructId<Guid>;)
  2. The type must be file-scoped, which automatically prevents polluting your assembly with types that aren't intended for direct consumption outside the template file itself.
  3. The template can optionally declare the type of ID value it supports by introducing the primary constructor with a single parameter named Value of that type.
  4. The record itself must be named TSelf.

The template itself can introduce arbitrary code that will be emitted for each matching struct id (i.e. all struct ids whose value type implements IUtf8SpanFormattable in this case). In this example, the template simply offers a pass-through implementation of the IUtf8SpanFormattable value.

As another example, imagine you have some standardized way of treating IDs in your application, by providing an interface for them, which applies to all Guid-based IDs:

public interface IId
{
    public Guid Id { get; }
}

You can now create a template that will automatically provide this interface for all struct ids that use Guid as their value type as follows:

[TStructId]
file partial record struct TSelf(Guid Value) : IId
{
    public Guid Id => Value;
}

This template is a proper C# compilation unit, so you can use any C# feature that your project supports, since its output will also be emitted via a source generator in the same project for matching struct ids.

In the case of a struct id defined as follows:

public partial record struct PersonId : IStructId<Guid>;

The template will be applied automatically and result in a partial declaration like:

partial record struct PersonId : IId
{
    public Guid Id => Value;
}

Things to note at template expansion time:

  1. The [TStructId] attribute is removed from the generated type automatically.
  2. The TSelf type is replaced with the actual name of the struct id.
  3. The primary constructor on the template is removed since it is already provided by anoother generator.

Dogfooding

CI Version Build

We also produce CI packages from branches and pull requests so you can dogfood builds as quickly as they are produced.

The CI feed is https://pkg.kzu.app/index.json.

The versioning scheme for packages is:

  • PR builds: 42.42.42-pr[NUMBER]
  • Branch builds: 42.42.42-[BRANCH].[COMMITS]

Sponsors

Clarius Org Kirill Osenkov MFB Technologies, Inc. Torutek DRIVE.NET, Inc. Keith Pickford Thomas Bolon Kori Francis Toni Wenzel Uno Platform Dan Siegel Reuben Swartz Jacob Foshee Eric Johnson Ix Technologies B.V. David JENNI Jonathan Charley Wu Jakob Tikjøb Andersen Tino Hager Mark Seemann Ken Bonny Simon Cropp agileworks-eu sorahex Zheyu Shen Vezel ChilliCream 4OTC Vincent Limo Jordan S. Jones domischell

Sponsor this project  

Learn more about GitHub Sponsors

About

Automatic implemention of stronly typed IDs via record structs with compile-time feature auto-detection

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published

Contributors 3

  •  
  •  
  •