Giter VIP home page Giter VIP logo

stronglytypedid's Introduction

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 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:

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: 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.

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. 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>
  
  <ItemGroup>
    <!-- Add the package -->
    <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 or C# 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:

An error is shown when the template does not exist

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:

Showing the CodeFix in action, suggesting you can add a project

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:

  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>
    <!--  Define the MSBuild constant    -->
    <DefineConstants>STRONGLY_TYPED_ID_EMBED_ATTRIBUTES</DefineConstants>
  </PropertyGroup>

  <!-- Add the package -->
  <PackageReference Include="StronglyTypedId" Version="1.0.0-beta08" 
                    PrivateAssets="all"
                    ExcludeAssets="compile;runtime" />
<!--                               ☝ Add compile to the list of excluded assets. -->

</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>
    <!--  Define the MSBuild constant to preserve usages   -->
    <DefineConstants>STRONGLY_TYPED_ID_USAGES</DefineConstants>
  </PropertyGroup>

  <!-- Add the package -->
  <PackageReference Include="StronglyTypedId" Version="1.0.0-beta08" PrivateAssets="all" />
  <!--              ☝ You must not exclude the runtime assets in this case -->

</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.

stronglytypedid's People

Contributors

andrewlock avatar jo-goro avatar khitiara avatar nielspilgaard avatar nxsoftware avatar stevedunn avatar vdurante avatar vebbo2 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stronglytypedid's Issues

Implement missing Guid(string) constructor

When implementing a type like

[StronglyTypedId(backingType: StronglyTypedIdBackingType.Guid)]
public partial struct CustomerId { }

Then instead of

var customerId = new CustomerId(new Guid("04004919-0a3a-47b5-89e0-92215ab9ff0f"))

I want to write

var customerId = new CustomerId("04004919-0a3a-47b5-89e0-92215ab9ff0f"). 

StronglyTypedId generates incorrect OpenApi specification

Hello,

I have a Strongly Typed Id struct ConsentCollectionId

[StronglyTypedId(backingType: StronglyTypedIdBackingType.Guid, StronglyTypedIdConverter.SystemTextJson)]
public partial struct ConsentCollectionId { }

This type is used as a parameter in one of my controllers. Unfortunately, the generated open-api specification file contains the wrong type description:
image

Instead of

{
  "value": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

it should be just

"3fa85f64-5717-4562-b3fc-2c963f66afa6"

Do you know if there is a way to change this?
As a work-around, I changed the type of the parameter back to Guid and do the mapping manually.

Kind regards,
Ilia

Make the "Empty" field optional

I had a bunch of public static readonly instances defined, and used reflection to bundle them all up into a readonly list at startup. The Empty value crept into that list by accident. I have other ways of fixing that little issue, but I thought maybe it would be a good idea to be able to suppress the Empty if someone needs to.

And since this is my first time communicating with you ... thank you Andrew, Great job :)

Rename Int to Int32

As suggested by @thomaslevesque

Just a small remark: Int should probably be renamed to Int32; int is the C# alias for Int32, but could mean other things in other languages (which, of course, isn't very important since it only generates C# code...)

`long` is read as `Int32`

The SystemTextJsonConverter for long backing fields reads from the JSON reader as reader.GetInt32(); instead of Int64

After starting your project, nuget stopped working

  1. Clone Project
  2. Build
  3. Remove project

And than for all projects:

dotnet restore
Determining projects to restore...
C:\Program Files\dotnet\sdk\5.0.104\NuGet.targets(131,5): error : The local source 'C:\Users*\source\repos*\StronglyTypedId\artifacts' doesn't exist. [C:\Users*\source\repos*\Test**.sln]

[Proposal] Add attribute on entity, not on separate struct

Your approach requires some bit of code that could be omitted, I believe. My way is something like:

[StronglyTypedId]
public partial class Foo {
  public IdType Id { get;set; }
}

compiles to:

public partial class Foo {
  public struct IdType {
    // Implementation here
  }
}

CS0436 Warning

When defining strong ids in a project and a child project which references the parent, we're seeing new CS0436 errors like this:

19>C:\Users\kfrancis\Projects\supercoolproject\aspnet-core\test\supercoolproject.Web.Tests\StronglyTypedIds\StronglyTypedIds.StronglyTypedIdGenerator\StronglyTypedIdDefaultsAttribute.cs(24,62,24,92): warning CS0436: The type 'StronglyTypedIdImplementations' in 'StronglyTypedIds\StronglyTypedIds.StronglyTypedIdGenerator\StronglyTypedIdImplementations.cs' conflicts with the imported type 'StronglyTypedIdImplementations' in 'supercoolproject.Application.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'StronglyTypedIds\StronglyTypedIds.StronglyTypedIdGenerator\StronglyTypedIdImplementations.cs'.
19>C:\Users\kfrancis\Projects\supercoolproject\aspnet-core\test\supercoolproject.Web.Tests\StronglyTypedIds\StronglyTypedIds.StronglyTypedIdGenerator\StronglyTypedIdAttribute.cs(25,62,25,92): warning CS0436: The type 'StronglyTypedIdImplementations' in 'StronglyTypedIds\StronglyTypedIds.StronglyTypedIdGenerator\StronglyTypedIdImplementations.cs' conflicts with the imported type 'StronglyTypedIdImplementations' in 'supercoolproject.Application.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'StronglyTypedIds\StronglyTypedIds.StronglyTypedIdGenerator\StronglyTypedIdImplementations.cs'.

Exception is thrown when deserializing nullable strongly-typed id

[StronglyTypedId]
public partial struct GroupId
{
}

public class User
{
    public User(GroupId? groupId)
    {
        GroupId = groupId;
    }

    public GroupId? GroupId { get; }
}

var json = JsonConvert.SerializeObject(new User(groupId: null)); // "{\"GroupId\":null}"
var deserialize = JsonConvert.DeserializeObject<User>(json); // throws Newtonsoft.Json.JsonSerializationException: 'Error converting value {null} to type 'System.Guid'. Path 'GroupId', line 1, position 15.'

The issue is caused by Newtonsoft.Json's behaviour (ReadJson is called on GroupIdNewtonsoftJsonConverter with objectType = Nullable<GroupId>).

A couple of related issues from Newtonsoft.Json:
JamesNK/Newtonsoft.Json#1783
JamesNK/Newtonsoft.Json#2219

Version of StronglyTypeId: 0.2.1
Version of Newtonsoft.Json: tried with both 12.0.3 and 13.0.1

[Proposal] Add shared interfaces to identify ids

I suggest to add an additional project (StronglyTypedId.Interfaces) with

public interface IId<TBacking>
{
    TBacking Value { get; }
}

and probably with

public interface IId<TId, TBacking>
    where TId : IId<TId, TBacking>
{
    static abstract TId New();

   // Parse/TryParse
}

IId interfaces will drammatically help us to minimize boilerplate code:

  1. Ids are stored in redis (and in rdbms via dapper) and we write the same (de)serialization code again and again, shared Parse/TryParse will help here
  2. We configure swagger to treat ids as guids and we have to explicitely enumerate all ids during configuration. With IId in place it will be possible to enumerate all ids via reflection.

We store ids in separate libraries with contracts and we do not to pollute ids with additional attributes or add swagger/dapper packages to contract libraries.

Can not use with Dapper.Contrib InsertAsync and MySQL

Summary

I'm having trouble integrating this library into my project which uses Dapper.Contrib to insert entities to a mysql database.

Dapper.Contrib throws System.InvalidCastException: 'Invalid cast from 'System.UInt64' to 'StronglyTypedIdTest.PersonId'. when calling InsertAsync. This is because of a combination of mysql's LAST_INSERT_ID() returning a ulong and Dapper.Contrib's InsertAsync function for mysql using LAST_INSERT_ID() and using the method Convert.ChangeType(id, idp.PropertyType) to set the Id property on the object.

Code to Reproduce

CREATE TABLE Persons(Id INT PRIMARY KEY AUTO_INCREMENT, Name VARCHAR(100));
using Dapper.Contrib.Extensions;
using MySqlConnector;
using StronglyTypedIds;

[assembly: StronglyTypedIdDefaults(
    backingType: StronglyTypedIdBackingType.Int,
    converters: StronglyTypedIdConverter.DapperTypeHandler | StronglyTypedIdConverter.TypeConverter)]

namespace StronglyTypedIdTest;


[StronglyTypedId]
public partial struct PersonId { }

public class Person
{
    [Key]
    public PersonId Id { get; set; }

    public string Name { get; set; }
}

public static class Program
{
    private const string ConnectionString = "xxxxxxxxxx";

    public static async Task Main()
    {
        Dapper.SqlMapper.AddTypeHandler(new PersonId.DapperTypeHandler());
        await using var conn = new MySqlConnection(ConnectionString);
        await conn.OpenAsync();
        await conn.InsertAsync(new Person { Name = "Andrew" });
    }
}
  <ItemGroup>
    <PackageReference Include="Dapper.Contrib" Version="2.0.78" />
    <PackageReference Include="MySqlConnector" Version="2.1.0" />
    <PackageReference Include="StronglyTypedId" Version="1.0.0-beta05">
	    <PrivateAssets>all</PrivateAssets>
	    <ExcludeAssets>runtime</ExcludeAssets>
    </PackageReference>
  </ItemGroup>

Solution

Unfortunately the way that the conversion happens with Convert.ChangeType makes it difficult to find a real solution. I wanted to log this issue to see if anybody else can figure one out.

Thanks for the great library and associated articles.

Unable to get SQL Server Identity working properly

I'm trying to convert an existing Int32 Id property into a strongly typed Id but I'm having trouble getting EF Core 6.0 to do inserts for me.

It's a standard Int32 Id property with SQL Server generating my values for me but no matter what settings I try in my EF config I keep getting errors such as:

The property 'Tag.Id' does not have a value set and no value generator is available for properties of type 'TagId'. Either set a value for the property before adding the entity or configure a value generator for properties of type 'TagId' in 'OnModelCreating'.

I've gone through the posts on the blog and the documentation here and I can't see anything that would help me?

Possible Bug

When I tried to access a route in ASP.NET with a strongly typed paramter I kept getting the following error:

Model bound complex types must not be abstract or value types and must have a parameterless constructor.

I had real trouble finding the problem as I had the code working prior to using the StronglyTypedId library itself. After much hunting I found the offending line:

return sourceType == typeof(int) || base.CanConvertFrom(context, sourceType);

In the example on your website here typeof(string) is used in place of typeof(int). The example is for a Guid and not an int but the behaviour is the same when used with ASP.NET routes (which is what I thought that snippet was for). FWIW the Long template also references int. Shall I fix?

StronglyTypedId Model Validation Problem

When I use OrderId in Controller/Action with wrong value (invalid guid), expected behavior is to fire a validation error. But the current behavior does not get into the validation and throws exception on

public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
    var guid = serializer.Deserialize<System.Guid>(reader); <------ here
    return new ReferralId(guid);
}

sample code for demonstration

public IActionResult (MyModel model){
     if (!ModelState.IsValid)
           return BadRequest(ModelState)

    .....
}

This throws exception before entering Action

[17:57:09 ERR] An unhandled exception has occurred while executing the request.
System.NullReferenceException: Object reference not set to an instance of an object.
   at Newtonsoft.Json.JsonSerializer.Deserialize[T](JsonReader reader)
   at ReferralId.ReferralIdNewtonsoftJsonConverter.ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) in C:\ASB\source\Workspaces\Workspace\Atlas\Atlas\Atlas.Module.HR\obj\Debug\net5.0\ReferralEntity.K3SYoa3Q.generated.cs:line 84
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonInputFormatter.ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
   at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder.BindModelAsync(ModelBindingContext bindingContext)
   at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync(ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor parameter, ModelMetadata metadata, Object value, Object container)
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Atlas.Infrastructure.Middleware.ExceptionHandlingMiddleware.InvokeAsync(HttpContext context) in C:\ASB\source\Workspaces\Workspace\Atlas\Atlas\Atlas.Infrastructure\Middleware\ExceptionHandlingMiddleware.cs:line 24
   at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.Invoke(HttpContext context)

Limits

Allow to limit string to 50 chars and ushort to 14 bits. I want to use this in games.

Add support for NewId

NewId is a sequential id generator analagous to sequential guids based on erlang's flake, and is the underlying id type i need to use for a few of my projects so having support would be ideal

Nested StronglyTypedId 1.0.0-beta02

Hello @andrewlock,
I am migrating from 0.2.1 to 1.0.0-beta02 and I have noticed that nesting:

public partial class Blog
{
  [StronglyTypedId(backingType: StronglyTypedIdBackingType.String, converters: StronglyTypedIdConverter.NewtonsoftJson | StronglyTypedIdConverter.SystemTextJson | StronglyTypedIdConverter.EfCoreValueConverter)]
  public partial struct PostId { }
}

Stopped triggering code generation, so calling a constructor is not possible and dotnet sdk 5.0.402 returns:
error CS1729: 'Blog.PostId' does not contain a constructor that takes 1 arguments

While it was working for 0.2.1. For now, I have just moved PostId outside to be directly under namespace scope and prefixed its name, then done some Find&Replace action.

[Sugestion] Add Parameterless Constructor to EfCoreValueConverter

With EF Core 6 the possibility of global conversions was introduced. But to set them up a parameterless Constructor is needed.
Currently this Feature is not documented but a pullrequest is on the way: https://github.com/dotnet/EntityFramework.Docs/pull/3716/commits. The only change needed to get it to work ist adding the parameterless constructor to the EfCoreValueConverter. After that the only thing the user has to do ist to add the converters in the ConfigureConventions method override.

Nullable support in EF Core

What's the best way to add nullable support here? You discuss it in previous posts, but it doesn't seem to be part of the library.

The property 'Claim.CaseId' could not be mapped, because it is of type 'Nullable' which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'

CaseId is defined:

[StronglyTypedId(jsonConverter: StronglyTypedIdJsonConverter.SystemTextJson | StronglyTypedIdJsonConverter.NewtonsoftJson, backingType: StronglyTypedIdBackingType.Guid)]
    public partial struct CaseId { }

And used:

public class Claim {
    public CaseId? CaseId { get; set; }
}

I have also:

public class CaseIdValueConverter : ValueConverter<CaseId, Guid>
    {
        public CaseIdValueConverter(ConverterMappingHints mappingHints = null)
            : base(
                id => id.Value,
                value => new CaseId(value),
                mappingHints
            )
        { }
    }

and ReplaceService<IValueConverterSelector, StronglyTypedIdValueConverterSelector>()

Mark generated code as generated code

To prevent code analysis warnings and errors.
Please add [System.CodeDom.Compiler.GeneratedCode(null, null)] to all generated code, with appropriated tool and version number.

[Proposal] Consider optional support for `class` vs. `struct`

Hi Andrew. In your excellent series of blog posts on Strongly Typed Id's, you make mention of some downsides to using a struct to model identity versus a class, due to implicit parameterless constructors. You also cite Vladamir Khorikov's article on the subject who makes further arguments for not choosing a struct for modelling identity and favoring a class. Classes have their downsides too of course, not least of which is having to null-check them all over the place, so they're not a panacea, but if you're trying to follow DDD best practices where protection of invariants is paramount, a class-based option would be desirable.

So, with all that said, have you considered enabling class-based strongly typed identifiers in your library in addition to struct-based ones?

Use System.Text.Json instead of Newtonsoft

ASP.Net Core 3.1 uses System.Text.Json to serialize objects. It would be good to support this new method of serialization.

I currently use the following (obviously it would need adapting to support the relevant use cases in this package).

public class MemberIdConverter : JsonConverter<MemberId>
{
    public override MemberId Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options) =>
            new MemberId(Guid.Parse(reader.GetString()));

    public override void Write(
        Utf8JsonWriter writer,
        MemberId value,
        JsonSerializerOptions options) =>
            writer.WriteStringValue(value.Value);
}

This issue depends upon #5

Type is not a supported Dictionary key type

We love the idea of strongly typed ids and we're using the library.

When trying to finally get this working in a generic repository, efcore, json to client side scenario - we're getting the following:

WARN The type 'CabMD.MasterNumbers.Master_Number' is not a supported Dictionary key type. The unsupported member type is located on type 'CabMD.MasterNumbers.MasterNumber'. Path: $.

System.NotSupportedException: The type 'CabMD.MasterNumbers.Master_Number' is not a supported Dictionary key type. The unsupported member type is located on type 'CabMD.MasterNumbers.MasterNumber'. Path: $.
---> System.NotSupportedException: The type 'CabMD.MasterNumbers.Master_Number' is not a supported Dictionary key type.
at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(Type keyType)
at System.Text.Json.JsonSerializerOptions.GetDictionaryKeyConverter(Type keyType)
at System.Text.Json.Serialization.Converters.DictionaryDefaultConverter3.GetKeyConverter(Type keyType, JsonSerializerOptions options) at System.Text.Json.Serialization.Converters.IDictionaryOfTKeyTValueConverter3.OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.Converters.DictionaryDefaultConverter3.OnTryWrite(Utf8JsonWriter writer, TCollection dictionary, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonConverter1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state) --- End of inner exception stack trace --- at System.Text.Json.ThrowHelper.ThrowNotSupportedException(WriteStack& state, NotSupportedException ex) at System.Text.Json.Serialization.JsonConverter1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter1.WriteCoreAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.JsonSerializer.WriteCore[TValue](JsonConverter jsonConverter, Utf8JsonWriter writer, TValue& value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.JsonSerializer.WriteCore[TValue](Utf8JsonWriter writer, TValue& value, Type inputType, JsonSerializerOptions options) at System.Text.Json.JsonSerializer.Serialize[TValue](Utf8JsonWriter writer, TValue& value, Type type, JsonSerializerOptions options) at System.Text.Json.JsonSerializer.Serialize[TValue](Utf8JsonWriter writer, TValue value, JsonSerializerOptions options) at Volo.Abp.Json.SystemTextJson.JsonConverters.ObjectToInferredTypesConverter.Write(Utf8JsonWriter writer, Object objectToWrite, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonConverter1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.JsonPropertyInfo1.GetMemberAndWriteJson(Object obj, WriteStack& state, Utf8JsonWriter writer) at System.Text.Json.Serialization.Converters.ObjectDefaultConverter1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonConverter1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter1.WriteCoreAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.JsonSerializer.WriteCore[TValue](JsonConverter jsonConverter, Utf8JsonWriter writer, TValue& value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.JsonSerializer.WriteCore[TValue](Utf8JsonWriter writer, TValue& value, Type inputType, JsonSerializerOptions options) at System.Text.Json.JsonSerializer.Serialize[TValue](TValue& value, Type inputType, JsonSerializerOptions options) at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options) at Volo.Abp.Json.SystemTextJson.AbpSystemTextJsonSerializerProvider.Serialize(Object obj, Boolean camelCase, Boolean indented) at Volo.Abp.Json.AbpHybridJsonSerializer.Serialize(Object obj, Boolean camelCase, Boolean indented) at Volo.Abp.Caching.Utf8JsonDistributedCacheSerializer.Serialize[T](T obj) at Volo.Abp.Caching.DistributedCache2.<>c__DisplayClass49_0.<g__SetRealCache|0>d.MoveNext()

This is defined like the follow:

    [StronglyTypedId(jsonConverter: StronglyTypedIdJsonConverter.SystemTextJson | StronglyTypedIdJsonConverter.NewtonsoftJson, backingType: StronglyTypedIdBackingType.String)]
    public partial struct Master_Number { }

We're using the StronglyTypedId library, v0.2.1

char identity

emojis are very versatile now in unicode - may be there is patter for meta identity?

CS1591-Missing XML comment for publicly visible type or member 'JobNumber.Value'

When creating an object like this:

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

We get a bunch of warnings/errors CS1591 and because we use the <TreatWarningsAsErrors>true</TreatWarningsAsErrors> we cannot compile the code.

Is is possible for you to add #pragma CS1591 to the sources?

Extensible Code Generation

We use serialisers such as protobuf-net and others, it would be great if an extension point be made such as adding attributes to members (one workaround might be to add custom serialisation methods to a partial class or could we add [DataMember(Order = 1)]/[DataContract] by default).

EF Core 3.1 doesn't autogenerate IDs

If I use a Guid based strongly typed id EF Core throws the following exception

System.InvalidOperationException : The property 'Foo.Id' could not be mapped, because it is of type 'FooId' which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

I can work around that issue by initializing the property

public class Foo
{
    public FooId Id { get; set; } = FooId.New();
}

but auto-generation by convention would be really nice.

Is that possible?

GetHashCode fails in certain cases when Backing Type is String

Hi!

Thanks for creating this library, I really like it. As I was using it within my project I had several ids with a backing type of String. On certain occasions, these ids were created using a default, parameterless constructor. When GetHashCode was invoked on those ids the NullReferenceException was thrown, and that what a bit against what I would expect.

I have attached a .CS file containing tests you can run to reproduce the issue (I had some issues with dotnetfiddle).

Tests.txt

Some of my thoughts on this:

  • I found out that in the current beta version, there is a NullableString as a backing type. This is ok and GetHashCode works without issues here in all scenarios, but I would expect that using String as a backing type is reliable. By this, I mean that it is ok that creating such a struct with the default constructor ends up with the Value property being null, but I would not expect GetHashCode to throw.

  • Creating structs with backing type String using default constructor (or default keyword) could potentially be undesirable. I could imagine some use cases where having such "id" would be ok, but Nullable String could be a better fit as a backing type. Could we have an analyzer that would raise a warning about creating structs with reference types like string, using the default constructor? This can prevent both intentional, but also accidental creation of such instances.

In the end, if you opt for addressing this, I would be happy to implement the solution and contribute to your project :)

Work around for UseIdentityColumn?

It looks like there's a bug in EF that prevents it from recognizing strongly typed IDs (longs / int64) as potential identity columns.

Identity value generation cannot be used for the property 'Id' on entity type 'MyTask' because the property type is 'TaskId'. Identity value generation can only be used with signed integer properties.

Has anyone found a way to avoid this? I'd put this in the EF Core repo, but I'm guessing it wouldn't be prioritized.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.