Tennisi.StronglyTypedId 1.0.25

There is a newer version of this package available.
See the version list below for details.
dotnet add package Tennisi.StronglyTypedId --version 1.0.25                
NuGet\Install-Package Tennisi.StronglyTypedId -Version 1.0.25                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Tennisi.StronglyTypedId" Version="1.0.25" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Tennisi.StronglyTypedId --version 1.0.25                
#r "nuget: Tennisi.StronglyTypedId, 1.0.25"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Tennisi.StronglyTypedId as a Cake Addin
#addin nuget:?package=Tennisi.StronglyTypedId&version=1.0.25

// Install Tennisi.StronglyTypedId as a Cake Tool
#tool nuget:?package=Tennisi.StronglyTypedId&version=1.0.25                

StronglyTypedId

StronglyTypedId logo

Build status NuGet

StronglyTypedId makes creating strongly-typed IDs as easy as adding an attribute! No more accidentally passing arguments in the wrong order to methods - StronglyTypedId uses .NET 6's compile-time incremental source generators to generate the boilerplate required to use strongly-typed IDs.

Simply, install the required package add the [StronglyTypedId] attribute to a struct (in the StronglyTypedIds namespace):

using StronglyTypedIds;
 
[StronglyTypedId] // <- Add this attribute to auto-generate the rest of the type
public partial struct FooId { }

and the source generator magically generates the backing code when you save the file! Use Go to Definition to see the generated code:

<img src="https://raw.githubusercontent.com/andrewlock/StronglyTypedId/master/docs/strongly_typed_id.gif" alt="Generating a strongly-typed ID using the StronglyTypedId packages"/>

StronglyTypedId requires requires the .NET Core SDK v6.0.100 or greater.

Changes in version 1.x

Version 0.x of this library used the helper library CodeGeneration.Roslyn by AArnott, for build-time source generation. In version 1.0.0 this approach has been completely replaced in favour of source generators, as these are explicitly supported in .NET 6+. As part of this change, there were a number of additional features added and breaking changes made.

Breaking Changes

  • StronglyTypedIds namespace is required. In version 0.x of the library, the [StronglyTypedId] attribute was in the global namespace. In version 1.x, the attribute is in the StronglyTypedIds namespace, so you must add namespace StronglyTypedIds;.
  • The properties exposed by StronglyTypedIds have changed: there is no longer a generateJsonConverter property. Instead, this is infered based on the StronglyTypedIdConverters flags provided.
  • The String backing typed ID will throw if you call the constructor with a null value

New Features

  • The attributes can now auto-generate additional converter types such as EF Core ValueConverter and Dapper TypeHandler, as described in my blog posts. These are optional flags on the converters property.
  • Made interface implementations (IEquatable<T> and IComparable<T> currently) optional. This is to potentially support additional interfaces in future versions.
  • Added a NullableString backing type. Due to the behaviour of structs in c#, the String backing type ID may still be null, but you can't explicitly call the constructor with a null value. In contrast, you can do this with the NullableString backing type.
  • Added a [StronglyTypedIdDefaults] attribute to set default values for all [StronglyTypedId] attributes in your project. This is useful if you want to customise all the attributes, for example, if you want to generate additional converters by default. You can still override all the properties of a [StronglyTypedId] instance.

Bug Fixes

  • Some converters had incorrect implementations, such as in (#24). These have been addressed in version 1.x.
  • Better null handling has been added for the String backing type, handling issues such as #32.
  • The code is marked as auto generated, to avoid errors such as #CS1591 as described in #27
  • An error deserializing nullable StronglyTypedIds with Newtonsoft.Json #36

Installing

To use the the StronglyTypedId NuGet package, install the StronglyTypedId package into your project. Depending on which converters you implement, you may need one or more of the following additional packages

To install the packages, add the references to your csproj file, for example by running

dotnet add package StronglyTypedId --version 1.0.0-beta06

This adds a <PackageReference> to your project. You can additionally mark the package as PrivateAsets="all" and ExcludeAssets="runtime".

Setting PrivateAssets="all" means any projects referencing this one will not also get a reference to the StronglyTypedId package. Setting ExcludeAssets="runtime" ensures the StronglyTypedId.Attributes.dll file is not copied to your build output (it is not required at runtime).

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
  
  
  <PackageReference Include="StronglyTypedId" Version="1.0.0-beta06" PrivateAssets="all" ExcludeAssets="runtime" />
  

</Project>

Usage

To create a strongly-typed ID, create a partial struct with the desired name, and decorate it with the [StronglyTypedId] attribute, in the StronglyTypedIds namespace:

using StronglyTypedIds;

[StronglyTypedId] // Add this attribute to auto-generate the rest of the type
public partial struct FooId { }

This generates the "default" strongly-typed ID using a Guid backing field, a custom TypeConverter, and a custom JsonConverter based on Newtonsoft.Json.

Customising the converters

You can customise which converters to generate by using flags. For example, to generate a TypeConverter, a System.Text.JsonConverter, and an EF Core ValueConverter, use

using StronglyTypedIds;

[StronglyTypedId(converters: StronglyTypedIdConverter.TypeConverter | StronglyTypedIdConverter.SystemTextJson | StronglyTypedIdConverter.EfCoreValueConverter)] 
public partial struct SystemTextJsonConverterId { }

Using different types as a backing fields

The default strongly-typed ID uses a Guid backing field:

using StronglyTypedIds;

[StronglyTypedId]
public partial struct FooId { }

var id = new FooId(Guid.NewGuid());

You can choose a different type backing field, by passing a value of the StronglyTypedIdBackingType enum in the constructor.

using StronglyTypedIds;

[StronglyTypedId(backingType: StronglyTypedIdBackingType.String)]
public partial struct FooId { }

var id = new FooId("my-id-value");

Currently supported values are Guid (the default), int, long, and string.

Changing the defaults globally

If you wish to change the converters, backing types, or implementations used by default for all the [StronglyTypedId]-decorated IDs in your project, you can use the assembly attribute [StronglyTypedIdDefaults] to set all of these. For example, the following sets the default converter to a whole project to [SystemTextJson], and changes the default backing-type to an int

// Set the defaults for the project
[assembly:StronglyTypedIdDefaults(
    backingType: StronglyTypedIdBackingType.Int,
    converters: StronglyTypedIdConverter.SystemTextJson)]

[StronglyTypedId]
public partial struct OrderId { }

[StronglyTypedId]
public partial struct UserId { } 

This is equivalent to setting these values manually on all the IDs:

[StronglyTypedId(
    backingType: StronglyTypedIdBackingType.Int,
    converters: StronglyTypedIdConverter.SystemTextJson)]
public partial struct OrderId { }

[StronglyTypedId(
     backingType: StronglyTypedIdBackingType.Int,
    converters: StronglyTypedIdConverter.SystemTextJson)]
public partial struct UserId { }

OpenApi/Swagger Specification

If you wish to use an ID in your Swagger models and want to have schema and model sample reflecting the ID backingfield type you will need:

  • Install Swagger Annotations >=5.0.0
  • Enable annotation in swagger gen with services.AddSwaggerGen(c => c.EnableAnnotations());
  • Use the converter flag StronglyTypedIdConverter.SwaggerSchemaFilter on the ID decorator. eg:
    [StronglyTypedId(
        backingType: StronglyTypedIdBackingType.Int,
        converters: StronglyTypedIdConverter.SwaggerSchemaFilter | StronglyTypedIdConverter.SystemTextJson)]
    public partial struct UserId { }
    

Embedding the attributes in your project

By default, the [StronglyTypedId] attributes referenced in your application are contained in an external dll. It is also possible to embed the attributes directly in your project, so they appear in the dll when your project is built. If you wish to do this, you must do two things:

  1. Define the MSBuild constant STRONGLY_TYPED_ID_EMBED_ATTRIBUTES. This ensures the attributes are embedded in your project
  2. Add compile to the list of excluded assets in your <PackageReference> element. This ensures the attributes in your project are referenced, instead of the StronglyTypedId.Attributes.dll library.

Your project file should look something like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    
    <DefineConstants>STRONGLY_TYPED_ID_EMBED_ATTRIBUTES</DefineConstants>
  </PropertyGroup>

  
  <PackageReference Include="StronglyTypedId" Version="1.0.0-beta06" 
                    PrivateAssets="all"
                    ExcludeAssets="compile;runtime" />


</Project>

Preserving usages of the [StronglyTypedId] attribute

The [StronglyTypedId] and [StronglyTypedIdDefaults] attributes are decorated with the [Conditional] attribute, so their usage will not appear in the build output of your project. If you use reflection at runtime on one of your IDs, you will not find [StronglyTypedId] in the list of custom attributes.

If you wish to preserve these attributes in the build output, you can define the STRONGLY_TYPED_ID_USAGES MSBuild variable. Note that this means your project will have a runtime-dependency on StronglyTypedId.Attributes.dll so you need to ensure this is included in your build output.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    
    <DefineConstants>STRONGLY_TYPED_ID_USAGES</DefineConstants>
  </PropertyGroup>

  
  <PackageReference Include="StronglyTypedId" Version="1.0.0-beta06" PrivateAssets="all" />
  

</Project>

Error CS0436 and [InternalsVisibleTo]

In the latest version of StronglyTypedId, you should not experience error CS0436 by default.

In previous versions of the StronglyTypedId generator, the [StronglyTypedId] attributes were added to your compilation as internal attributes by default. If you added the source generator package to multiple projects, and used the [InternalsVisibleTo] attribute, you could experience errors when you build:

warning CS0436: The type 'StronglyTypedIdImplementations' in 'StronglyTypedIds\StronglyTypedIds.StronglyTypedIdGenerator\StronglyTypedIdImplementations.cs' conflicts with the imported type 'StronglyTypedIdImplementations' in 'MyProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

In the latest version of StronglyTypedId, the attributes are not embedded by default, so you should not experience this problem. If you see this error, compare your installation to the examples in the installation guide.

Why do I need this library?

I have written a blog-post series on strongly-typed IDs that explains the issues and rational behind this library. For a detailed view, I suggest starting there, but I provide a brief introduction here.

This library is designed to tackle a specific instance of primitive obsession, whereby we use primitive objects (Guid/string/int/long etc) to represent the IDs of domain objects. The problem is that these IDs are all interchangeable - an order ID can be assigned to a product ID, despite the fact that is likely nonsensical from the domain point of view. See here for a more concrete example.

By using strongly-typed IDs, we give each ID its own Type which wraps the underlying primitive value. This ensures you can only use the ID where it makes sense: ProductIds can only be assigned to products, or you can only search for products using a ProductId, not an OrderId.

Unfortunately, taking this approach requires a lot of boilerplate and ceremony to make working with the IDs manageable. This library abstracts all that away from you, by generating the boilerplate at build-time by using a Roslyn-powered code generator.

What code is generated?

The exact code generated depends on the arguments you provide to the StronglyTypedId attribute. The code is generated to the obj folder of the project, so you can use Go to Definition on your Id to see the exact code generated in each case.

You can see see example implementations in the test SourceGenerationHelperSnapshotTests in which all permutations of the attribute are tested, and examples generated in the snapshots folder.

Requirements

The StronglyTypedId NuGet package is a .NET Standard 2.0 package.

You must be using the .NET 6+ SDK (though you can compile for other target frameworks like .NET Core 2.1 and .NET Framework 4.8)

The structs you decorate with the StronglyTypedId attribute must be marked partial.

Credits

StronglyTypedId wouldn't work if not for AArnott's CodeGeneration.Roslyn library.

The build process and general design of the library was modelled on the RecordGenerator project, which is similar to this project, but can be used to generate immutable Record types.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.1.31 906 12/12/2023
1.1.30 109 12/12/2023
1.1.27 344 11/7/2023
1.1.26 131 11/7/2023
1.1.25 136 11/7/2023
1.1.16 137 11/5/2023
1.1.15 311 8/21/2023
1.1.14 157 8/21/2023
1.1.13 146 8/21/2023
1.1.11 170 8/21/2023
1.1.10 170 8/21/2023
1.1.9 179 8/21/2023
1.1.5 163 8/21/2023
1.1.4 149 8/21/2023
1.1.3 169 8/21/2023
1.1.2 225 6/26/2023
1.1.1 159 6/26/2023
1.0.96 200 5/27/2023
1.0.94 215 4/11/2023
1.0.91 200 4/11/2023
1.0.90 245 4/5/2023
1.0.85 185 4/5/2023
1.0.82 196 4/5/2023
1.0.80 210 4/5/2023
1.0.79 232 4/3/2023
1.0.73 227 4/2/2023
1.0.72 218 4/2/2023
1.0.71 224 4/2/2023
1.0.70 238 3/27/2023
1.0.69 230 3/27/2023
1.0.68 232 3/27/2023
1.0.63 227 3/27/2023
1.0.56 240 3/27/2023
1.0.54 235 3/23/2023
1.0.53 211 3/23/2023
1.0.35 238 3/23/2023
1.0.33 245 3/23/2023
1.0.32 226 3/23/2023
1.0.30 225 3/23/2023
1.0.26 209 3/23/2023
1.0.25 217 3/23/2023
1.0.24 223 3/23/2023
1.0.23 258 3/16/2023
1.0.20 223 3/16/2023
1.0.19 236 3/16/2023
1.0.18 234 3/14/2023
1.0.17 213 3/14/2023
1.0.16 222 3/14/2023
1.0.14 209 3/14/2023
1.0.11 222 3/14/2023
1.0.7 229 3/14/2023
1.0.6 225 3/14/2023
1.0.3 208 3/13/2023
1.0.2 228 3/13/2023
1.0.1 239 3/13/2023
1.0.0-beta07 162 3/7/2023
1.0.0-beta-08 156 3/13/2023