andrewlock / StronglyTypedId

A Rosyln-powered generator for strongly-typed IDs
MIT License
1.52k stars 79 forks source link

Type is not a supported Dictionary key type #29

Closed kfrancis closed 2 years ago

kfrancis commented 3 years ago

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

kfrancis commented 3 years ago

Any ideas?

andrewlock commented 3 years ago

Not off the top of my head. Could you provide an example of how you're using the id in your classes. It looks like the id is trying to be used as a dictionary key in JSON serialization; that seems unusual...

andrewlock commented 2 years ago

I'll close this as it's stale, but if you're still running into it, feel free to reopen

NxSoftware commented 2 years ago

I ran into this issue too when including a Strongly Typed ID as a dictionary key in the API request/response.

Package version: 1.0.0-beta06

See this minimal API Program.cs for reproduction:

using StronglyTypedIds;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var response = new Dictionary<UserId, int>();
response[UserId.New()] = 1;

app.MapPost("/", (IDictionary<UserId, int> request) => request);

app.Run();

[StronglyTypedId(converters: StronglyTypedIdConverter.SystemTextJson)]
partial struct UserId
{
}
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.NotSupportedException: The type 'UserId' is not a supported dictionary key using converter of type 'UserId+UserIdSystemTextJsonConverter'. The unsupported member type is located on type 'System.Collections.Generic.Dictionary`2[UserId,System.Int32]'. Path: $ | LineNumber: 1 | BytePositionInLine: 43.
       ---> System.NotSupportedException: The type 'UserId' is not a supported dictionary key using converter of type 'UserId+UserIdSystemTextJsonConverter'.
         at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(Type keyType, JsonConverter converter)
         at System.Text.Json.Serialization.JsonConverter`1.ReadAsPropertyName(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
         at System.Text.Json.Serialization.JsonConverter`1.ReadAsPropertyNameCore(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
         at System.Text.Json.Serialization.JsonDictionaryConverter`3.<OnTryRead>g__ReadDictionaryKey|10_0(Utf8JsonReader& reader, ReadStack& state, <>c__DisplayClass10_0& )
         at System.Text.Json.Serialization.JsonDictionaryConverter`3.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TDictionary& value)
         at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
         at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
         --- End of inner exception stack trace ---
         at System.Text.Json.ThrowHelper.ThrowNotSupportedException(ReadStack& state, Utf8JsonReader& reader, NotSupportedException ex)
         at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.ReadCoreAsObject(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
         at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
         at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase)
         at System.Text.Json.JsonSerializer.ContinueDeserialize[TValue](ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, JsonConverter converter, JsonSerializerOptions options)
         at System.Text.Json.JsonSerializer.ReadAllAsync[TValue](Stream utf8Json, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, Type type, JsonSerializerOptions options, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, Type type, JsonSerializerOptions options, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass46_3.<<HandleRequestBodyAndCompileRequestDelegate>b__2>d.MoveNext()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Screenshot 2022-07-14 at 10 04 32

The generated System.Text.Json converter can be updated to support this use-case now that custom converters can read & write dictionary keys (see https://github.com/dotnet/runtime/issues/46520):

using System.Text.Json;
using System.Text.Json.Serialization;
using StronglyTypedIds;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var response = new Dictionary<UserId, int>();
response[UserId.New()] = 1;

app.MapPost("/", (IDictionary<UserId, int> request) => request);

app.Run();

[StronglyTypedId(converters: StronglyTypedIdConverter.None)]
[JsonConverter(typeof(UserIdSystemTextJsonConverter))]
partial struct UserId
{
    class UserIdSystemTextJsonConverter : System.Text.Json.Serialization.JsonConverter<UserId>
    {
        public override UserId Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options)
        {
            return new UserId(System.Guid.Parse(reader.GetString()));
        }

        public override void Write(System.Text.Json.Utf8JsonWriter writer, UserId value, System.Text.Json.JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.Value);
        }

        public override void WriteAsPropertyName(Utf8JsonWriter writer, UserId value, JsonSerializerOptions options)
        {
            writer.WritePropertyName(value.Value.ToString());
        }

        public override UserId ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return new UserId(System.Guid.Parse(reader.GetString()));
        }
    }
}

Screenshot 2022-07-14 at 10 02 41

I've not been able to test this issue with Newtonsoft so I'm not sure if it even affects that converter nor what a potential solution would be.

What are your thoughts on this @andrewlock? Is this use-case unusual and should we be using the underlying Guid type as dictionary keys in our APIs or is this something that could be included in the library?

SasLuca commented 1 year ago

Ran into this issue as well. I dont see why this shouldn't be supported, seems like a good use-case to me.

@andrewlock any thoughts on reconsidering this?

benlongo commented 1 year ago

Just ran into this as well. I think it should be at least implemented for string types and possibly opt in for numerics

steffenskov commented 1 year ago

I'm not actually using Andrews library, as I had need for a .Net 5 solution (which has since evolved to .Net 7 as of writing). To that end I created something similar, although in a different fashion using inheritance and generics instead of a source generator. Incidentally this has opened quite a few possibilities up as well I've just today stumbled upon this very issue and resolved it in that library. If you're up for trying something different out (that aims to solve the same problem), you'll find it here: https://github.com/steffenskov/StrongTypedId

Other than that I can only support the solution that NxSoftware has posted, it does indeed solve the problem :+1: