StronglyTypedId.Templates
1.0.0-beta08
dotnet add package StronglyTypedId.Templates --version 1.0.0-beta08
NuGet\Install-Package StronglyTypedId.Templates -Version 1.0.0-beta08
<PackageReference Include="StronglyTypedId.Templates" Version="1.0.0-beta08" />
paket add StronglyTypedId.Templates --version 1.0.0-beta08
#r "nuget: StronglyTypedId.Templates, 1.0.0-beta08"
// Install StronglyTypedId.Templates as a Cake Addin #addin nuget:?package=StronglyTypedId.Templates&version=1.0.0-beta08&prerelease // Install StronglyTypedId.Templates as a Cake Tool #tool nuget:?package=StronglyTypedId.Templates&version=1.0.0-beta08&prerelease
StronglyTypedId
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 7+'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 the .NET Core SDK v7.0.100 or greater.
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: ProductId
s 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.
Requirements
The StronglyTypedId NuGet package is a .NET Standard 2.0 package.
You must be using the .NET 7+ SDK (though you can compile for other target frameworks like .NET Core 2.1 and .NET Framework 4.8)
Installing
To use StronglyTypedIds, install the StronglyTypedId NuGet package into your csproj file, for example by running
dotnet add package StronglyTypedId --version 1.0.0-beta08
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. SettingExcludeAssets="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>
<ItemGroup>
<PackageReference Include="StronglyTypedId" Version="1.0.0-beta08" PrivateAssets="all" ExcludeAssets="runtime" />
</ItemGroup>
</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. You can use your IDE's Go to Definition functionality on your ID to see the_exact code generated by the source generator. The ID implements the following interfaces automatically:
IComparable<T>
IEquatable<T>
IFormattable
ISpanFormattable
(.NET 6+)IParsable<T>
(.NET 7+)ISpanParsable<T>
(.NET 7+)IUtf8SpanFormattable
(.NET 8+)IUtf8SpanParsable<T>
(.NET 8+)
And it additionally includes two converters/serializers:
System.ComponentModel.TypeConverter
System.Text.Json.Serialization.JsonConverter
This provides basic integration for many use cases, but you may want to customize the IDs further, as you'll see shortly.
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 Template
enum in the constructor.
using StronglyTypedIds;
[StronglyTypedId(Template.Int)]
public partial struct FooId { }
var id = new FooId(123);
Currently supported built-in backing types are:
Guid
(the default)int
long
string
Changing the defaults globally
If you wish to change the template 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 changes the default backing-type for all IDs to int
// Set the defaults for the project
[assembly:StronglyTypedIdDefaults(Template.Int)]
[StronglyTypedId] // Uses the default 'int' template
public partial struct OrderId { }
[StronglyTypedId] // Uses the default 'int' template
public partial struct UserId { }
[StronglyTypedId(Template.Guid)] // Overrides the default to use 'Guid' template
public partial struct HostId { }
Using custom templates
In addition to the built-in templates, you can provide your own templates for use with strongly typed IDs. To do this, do the following:
- Add a file to your project with the name TEMPLATE.typedid, where
TEMPLATE
is the name of the template - Update the template with your desired ID content. Use
PLACEHOLDERID
inside the template. This will be replaced with the ID's name when generating the template. - Update the "build action" for the template to
AdditionalFiles
orC# analyzer additional file
(depending on your IDE).
For example, you could create a template that provides an EF Core ValueConverter
implementation called guid-efcore.typedid like this:
partial struct PLACEHOLDERID
{
public class EfCoreValueConverter : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<PLACEHOLDERID, global::System.Guid>
{
public EfCoreValueConverter() : this(null) { }
public EfCoreValueConverter(global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ConverterMappingHints? mappingHints = null)
: base(
id => id.Value,
value => new PLACEHOLDERID(value),
mappingHints
) { }
}
}
Note that the content of the guid-efcore.typedid file is valid C#. One easy way to author these templates is to create a .cs file containing the code you want for your ID, then rename your ID to
PLACEHOLDERID
, change the file extension from .cs to _.typedid, and then set the build action.
After creating a template in your project you can apply it to your IDs like this:
// Use the built-in Guid template and also the custom template
[StronglyTypedId(Template.Guid, "guid-efcore")]
public partial struct GuidId {}
This shows another important feature: you can specify multiple templates to use when generating the ID.
Using multiple templates
When specifying the templates for an ID, you can specify
- 0 or 1 built-in templates (using
Template.Guid
etc) - 0 or more custom templates
For example:
[StronglyTypedId] // Use the default templates
public partial struct MyDefaultId {}
[StronglyTypedId(Template.Guid)] // Use a built-in template only
public partial struct MyId1 {}
[StronglyTypedId("my-guid")] // Use a custom template only
public partial struct MyId2 {}
[StronglyTypedId("my-guid", "guid-efcore")] // Use multiple custom templates
public partial struct MyId2 {}
[StronglyTypedId(Template.Guid, "guid-efcore")] // Use a built-in template _and_ a custom template
public partial struct MyId3 {}
// Use a built-in template _and_ multiple custom template
[StronglyTypedId(Template.Guid, "guid-efcore", "guid-dapper")]
public partial struct MyId4 {}
Similarly, for the optional [StronglyTypedIdDefaults]
assembly attribute, which defines the default templates to use when you use the raw [StronglyTypedId]
attribute, you use a combination of built-in and/or custom templates:
//⚠ You can only use _one_ of these in your project, they're all shown here for comparison
[assembly:StronglyTypedIdDefaults(Template.Guid)] // Use a built-in template only
[assembly:StronglyTypedIdDefaults("my-guid")] // Use a custom template only
[assembly:StronglyTypedIdDefaults("my-guid", "guid-efcore")] // Use multiple custom templates
[assembly:StronglyTypedIdDefaults(Template.Guid, "guid-efcore")] // Use a built-in template _and_ a custom template
// Use a built-in template _and_ multiple custom template
[assembly:StronglyTypedIdDefaults(Template.Guid, "guid-efcore", "guid-dapper")]
[StronglyTypedId] // Uses whatever templates were specified!
public partial struct MyDefaultId {}
To simplify the creation of templates, the StronglyTypedId package includes a code-fix provider to generate a template.
Creating a custom template with the Roslyn CodeFix provider
As well as the source generator, the StronglyTypedId NuGet package includes a CodeFix provider that looks for cases where you have specified a custom template that the source generator cannot find. For example, in the following code,the "some-int"
template does not yet exist:
[StronglyTypedId("some-int")] // does not exist
public partial struct MyStruct { }
In the IDE, you can see the generator has marked this as an error:
The image above also shows that there's a CodeFix action available. Clicking the action reveals the possible fix: Add some-int.typedid template to the project, and shows a preview of the file that will be added:
Choosing this option will add the template to your project.
Unfortunately, due to limitations with the Roslyn APIs, it's not possible to add the new template with the required AdditionalFiles/C# analyzer additional file build action already set. Until you change the build-action, the error will remain on your
[StronglyTypedId]
attribute.
Right-click the newly-added template, choose Properties, and change the Build Action to either C# analyzer additional file (Visual Studio 2022) or AdditionalFiles (JetBrains Rider). The source generator will then detect your template and the error will disappear.
The CodeFix provider does a basic check against the name of the template you're trying to create. If it includes int
, long
, or string
, the template it creates will be based on one of those backing types. Otherwise, the template is based on a Guid
backing type.
Once the template is created, you're free to edit it as required.
"Community" templates package StronglyTypedId.Templates
The "template-based" design of StronglyTypedId is intended to make it easy to get started, while also giving you the flexibility to customise your IDs to your needs.
To make it easier to share templates with multiple people, and optional StronglyTypedId.Templates NuGet package is available that includes various converters and other backing types. To use these templates, add the StronglyTypedId.Templates package to your project:
dotnet add package StronglyTypedId.Templates --version 1.0.0-beta08
You will then be able to reference any of the templates it includes. This includes "complete" implementations, including multiple converters, for various backing types:
guid-full
int-full
long-full
string-full
nullablestring-full
newid-full
It also includes "standalone" EF Core, Dapper, and Newtonsoft JSON converter templates to enhance the Guid
/int
/long
/string
built-in templates. For example
- Templates for use with
Template.Guid
guid-dapper
guid-efcore
guid-newtonsoftjson
- Templates for use with
Template.Int
int-dapper
int-efcore
int-newtonsoftjson
- Templates for use with
Template.Long
long-dapper
long-efcore
long-newtonsoftjson
- Templates for use with
Template.String
string-dapper
string-efcore
string-newtonsoftjson
For the full list of available templates, see GitHub.
You can also create your own templates package and distribute it on NuGet.
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:
- Define the MSBuild constant
STRONGLY_TYPED_ID_EMBED_ATTRIBUTES
. This ensures the attributes are embedded in your project - 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-beta08"
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-beta08" 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.
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 (2)
Showing the top 2 popular GitHub repositories that depend on StronglyTypedId.Templates:
Repository | Stars |
---|---|
andrewlock/StronglyTypedId
A Rosyln-powered generator for strongly-typed IDs
|
|
yanpitangui/dotnet-api-boilerplate
A Dotnet 9.0 WebApi template project. MediatR, Swagger, Mapper, Serilog and more implemented.
|
Version | Downloads | Last updated |
---|---|---|
1.0.0-beta08 | 132,148 | 4/25/2024 |
1.0.0-beta07 | 88,894 | 12/14/2023 |
## Changes in 1.0.0-beta08:
Fixes
* Fix bug in System.Text.Json converters that could cause errors when used with source generators
## Changes in 1.0.0-beta07:
⚠ WARNING ⚠
This version contains a fundamental breaking change in how you define your IDs
Breaking Changes:
* This release contains a fundamental change in how you define your IDs
* For details see the related issue (https://github.com/andrewlock/StronglyTypedId/issues/102), the PR (https://github.com/andrewlock/StronglyTypedId/pull/117), or the README
* The "options" enums `StronglyTypedIdBackingType`, `StronglyTypedIdConverter`, and `StronglyTypedIdImplementations` have been removed.
* Instead, you simply choose one of 4 different built-in templates, or use a custom template.
Features
* Adds `[GeneratedCode]` attribute to generated IDs (Fixes https://github.com/andrewlock/StronglyTypedId/issues/57)
* Add support for parsing `SCOPE_IDENTITY()` and `@@IDENTITY` in MSSQL with `DapperTypeHandler` (Fixes https://github.com/andrewlock/StronglyTypedId/issues/118)
* Fix exception being thrown when deserializing nullable strongly-typed id backed by string (Fixes https://github.com/andrewlock/StronglyTypedId/issues/83)
* Allow creating multiple IDs with the same name in a project (Fixes https://github.com/andrewlock/StronglyTypedId/issues/74, thanks @jo-goro https://github.com/andrewlock/StronglyTypedId/pull/77!)
## Changes in 1.0.0-beta06:
* Added support for Masstransit.NewId (thanks @Khitiara!) Fixes https://github.com/andrewlock/StronglyTypedId/issues/51
* Added parameterless constructor to EF Core ValueConverts for compatibility with global conventions. Fixes https://github.com/andrewlock/StronglyTypedId/issues/50
* Added `#pragma warning disable 1591` to generated code to avoid warning CS1591. Fixes https://github.com/andrewlock/StronglyTypedId/issues/47
## Changes in 1.0.0-beta05:
Breaking Changes:
* Removed StronglyTypedId.Attributes NuGet package.
* The attributes are no longer embed in your project by default, instead it will use the external dll. You can re-enable the embedding by setting `STRONGLY_TYPED_ID_EMBED_ATTRIBUTES`.
New Features:
* Improved approach to handling [InternalsVisibleTo] issues, by embedding the StronglyTypedId.Attributes.dll in the NuGet package directly.
## Changes in 1.0.0-beta04:
New Features:
* Added support for IDs inside nested classes/records/structs (Fixes https://github.com/andrewlock/StronglyTypedId/issues/40)
## Changes in 1.0.0-beta03:
Breaking Changes:
* Converted to use .NET 6's incremental source generators. This should provide performance improvements, but it requires using the .NET 6 SDK.
Bug fixes:
* Fixed problem deserializing nullable strongly-typed IDs with Newtonsoft.Json (https://github.com/andrewlock/StronglyTypedId/issues/36)
New Features:
* To support scenarios in which [InternalsVisibleTo] causes duplicate reference issues with the marker attributes, you can set the msbuild constant `STRONGLY_TYPED_ID_EXCLUDE_ATTRIBUTES` to exclude these from build output. You must then reference the StronglyTypedId.Attributes project as well, which contains the marker attributes.
* By default, the marker attributes are decorated with the `[Conditional]` attribute, so they will not appear on your IDs. If you need these to persist, define the msbuild constant `STRONGLY_TYPED_ID_USAGES`.
## Changes in 1.0.0-beta02:
Bug fixes
* Adds auto-generated attributes and enums as `internal` to help avoid referencing issues
## 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](https://andrewlock.net/series/using-strongly-typed-entity-ids-to-avoid-primitive-obsession/). 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 `struct`s 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 ([#26](https://github.com/andrewlock/StronglyTypedId/issues/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](https://github.com/andrewlock/StronglyTypedId/issues/32).
* The code is marked as auto generated, to avoid errors such as #CS1591 as described in [#27](https://github.com/andrewlock/StronglyTypedId/issues/27)
See https://github.com/andrewlock/StronglyTypedId/blob/master/CHANGELOG.md#v100 for more details.