使用 C# 9 的records作為強類型ID - JSON序列化


在本系列的上一篇文章中,我們注意到強類型ID的實體,序列化為 JSON 的時候報錯了,就像這樣:

{
    "id": {
        "value": 1
    },
    "name": "Apple",
    "unitPrice": 0.8
}

不過想了一下,這樣的意外也是在意料之中的,強類型ID是record類型,而不是原始類型,因此將其序列化為一個對象是有意義的,但這顯然不是我們想要的……讓我們看看如何解決這個問題。

System.Text.Json

在最新版本的ASP.NET Core(從3.0)中,默認的JSON序列化程序是System.Text.Json,因此讓我首先介紹這種。

為了將強類型的id序列化為其值而不是對象,我們需要編寫一個通用的 JsonConverter:

public class StronglyTypedIdJsonConverter<TStronglyTypedId, TValue> : JsonConverter<TStronglyTypedId>
    where TStronglyTypedId : StronglyTypedId<TValue>
    where TValue : notnull
{
    public override TStronglyTypedId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType is JsonTokenType.Null)
            return null;

        var value = JsonSerializer.Deserialize<TValue>(ref reader, options);
        var factory = StronglyTypedIdHelper.GetFactory<TValue>(typeToConvert);
        return (TStronglyTypedId)factory(value);
    }

    public override void Write(Utf8JsonWriter writer, TStronglyTypedId value, JsonSerializerOptions options)
    {
        if (value is null)
            writer.WriteNullValue();
        else
            JsonSerializer.Serialize(writer, value.Value, options);
    }
}

邏輯很簡單,對於直接讀取 id.value, 對於反序列化,創建一個強類型id的實例,然后給它賦值。

然后在啟動類中配置:

services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(
            new StronglyTypedIdJsonConverter<ProductId, int>());
    });

現在拿到了想要的結果:

{
    "id": 1,
    "name": "Apple",
    "unitPrice": 0.8
}

真好!不過,還有有一個問題:我們只為添加了一個對於ProductId的轉換器,但我不想為每種類型的強類型ID添加另一個轉換器!我們想要一個適用於所有強類型id的轉換器……,現在可以創建一個轉換器工廠(ConverterFactory),就像下邊這樣:

public class StronglyTypedIdJsonConverterFactory : JsonConverterFactory
{
    private static readonly ConcurrentDictionary<Type, JsonConverter> Cache = new();

    public override bool CanConvert(Type typeToConvert)
    {
        return StronglyTypedIdHelper.IsStronglyTypedId(typeToConvert);
    }

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        return Cache.GetOrAdd(typeToConvert, CreateConverter);
    }

    private static JsonConverter CreateConverter(Type typeToConvert)
    {
        if (!StronglyTypedIdHelper.IsStronglyTypedId(typeToConvert, out var valueType))
            throw new InvalidOperationException($"Cannot create converter for '{typeToConvert}'");

        var type = typeof(StronglyTypedIdJsonConverter<,>).MakeGenericType(typeToConvert, valueType);
        return (JsonConverter)Activator.CreateInstance(type);
    }
}

首先我們查看需要轉換的類型,檢查它是否實際上是強類型的id,然后為該類型創建特定轉換器的實例,我們添加了一些緩存,避免每次都進行反射工作。

現在,我們沒有添加特定的JsonConvert,只是添加了一個Factory,然后在啟動文件修改,現在,我們的轉換器將應用於每個強類型ID

services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(
            new StronglyTypedIdJsonConverterFactory());
    });

Newtonsoft.Json

如果您的項目使用的是Newtonsoft.Json進行JSON序列化,那就很簡單了。

當它序列化一個值時,Newtonsoft.Json 查找一個compatible JsonConverter,如果找不到,就查找一個TypeConverter, 如果TypeConverter存在,並且可以將值轉換為string,那么它把值序列化為字符串, 因為我們之前定義了 TypeConverter,Newtonsoft.Json查找到了,我得到以下結果:

{
    "id": "1",
    "name": "Apple",
    "unitPrice": 0.8
}

幾乎是正確的……除了id值不應序列化為字符串,而應序列化為數字,如果id值是GUID或字符串而不是int,那就很好,則需要編寫一個自定義轉換器。

它和 System.Text.Json 的轉換器非常相似,不同之處在於Newtonsoft.Json沒有轉換器工廠(ConvertFactory)的概念,相反,我們將編寫一個非泛型轉換器:

public class StronglyTypedIdNewtonsoftJsonConverter : JsonConverter
{
    private static readonly ConcurrentDictionary<Type, JsonConverter> Cache = new();

    public override bool CanConvert(Type objectType)
    {
        return StronglyTypedIdHelper.IsStronglyTypedId(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var converter = GetConverter(objectType);
        return converter.ReadJson(reader, objectType, existingValue, serializer);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value is null)
        {
            writer.WriteNull();
        }
        else
        {
            var converter = GetConverter(value.GetType());
            converter.WriteJson(writer, value, serializer);
        }
    }

    private static JsonConverter GetConverter(Type objectType)
    {
        return Cache.GetOrAdd(objectType, CreateConverter);
    }

    private static JsonConverter CreateConverter(Type objectType)
    {
        if (!StronglyTypedIdHelper.IsStronglyTypedId(objectType, out var valueType))
            throw new InvalidOperationException($"Cannot create converter for '{objectType}'");

        var type = typeof(StronglyTypedIdNewtonsoftJsonConverter<,>).MakeGenericType(objectType, valueType);
        return (JsonConverter)Activator.CreateInstance(type);
    }
}

public class StronglyTypedIdNewtonsoftJsonConverter<TStronglyTypedId, TValue> : JsonConverter<TStronglyTypedId>
    where TStronglyTypedId : StronglyTypedId<TValue>
    where TValue : notnull
{
    public override TStronglyTypedId ReadJson(JsonReader reader, Type objectType, TStronglyTypedId existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if (reader.TokenType is JsonToken.Null)
            return null;

        var value = serializer.Deserialize<TValue>(reader);
        var factory = StronglyTypedIdHelper.GetFactory<TValue>(objectType);
        return (TStronglyTypedId)factory(value);
    }

    public override void WriteJson(JsonWriter writer, TStronglyTypedId value, JsonSerializer serializer)
    {
        if (value is null)
            writer.WriteNull();
        else
            writer.WriteValue(value.Value);
    }
}

然后在啟動文件中這樣設置:

services.AddControllers()
        .AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.Converters.Add(
                new StronglyTypedIdNewtonsoftJsonConverter());
        });

然后,我們得到了預期的結果,輸出的結果是這樣:

{
    "id": 1,
    "name": "Apple",
    "unitPrice": 0.8
}

原文作者: thomas levesque
原文鏈接:https://thomaslevesque.com/2020/12/07/csharp-9-records-as-strongly-typed-ids-part-3-json-serialization/

最后

歡迎掃碼關注我們的公眾號 【全球技術精選】,專注國外優秀博客的翻譯和開源項目分享,也可以添加QQ群 897216102


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM