使用NewtonSoft,我可以在我的CurrentDictionary<string, Saturation>属性定义上指定以下属性,以指示我的自定义类的自定义转换器:'饱和度':

[JsonProperty(PropertyName = "substances", ItemConverterType = typeof(SaturationJsonConverter))]
protected ConcurrentDictionary<string, Saturation> Substances { get; } = new();

这里设置ItemConverterType将转换器应用于字典值而不是整个字典.

问:鉴于我已经为Saturation创建了自定义JsonConverter<Saturation>,如何使用System.Text.Json模拟这种行为?

public class SaturationJsonConverter : JsonConverter<Saturation>
{
    public override Saturation? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 
    {
        // Contents omitted
    }

    public override void Write(Utf8JsonWriter writer, Saturation? value, JsonSerializerOptions options) => 
    {
        // Contents omitted
    }
}

推荐答案

As noted in the question System.Text.Json: How to apply a JsonConverter for a collection with a custom converter for the collection's items?, System.Text.Json does not have an equivalent to Json.NET's JsonPropertyAttribute.ItemConverterType. Thus you will need to emulate it by creating a JsonConverter decorator that serializes and deserializes dictionaries using your custom value converter.

以下是一个这样的实现. 请注意,它只适用于string个键的字典:

public class DictionaryValueConverterDecorator<TItemConverter> : JsonConverterFactory where TItemConverter : JsonConverter, new()
{
    readonly TItemConverter itemConverter = new TItemConverter();

    public override bool CanConvert(Type typeToConvert) =>
        GetStringKeyedDictionaryValueType(typeToConvert) != null && GetConcreteTypeToConvert(typeToConvert) != null;

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        if (!(GetStringKeyedDictionaryValueType(typeToConvert) is {} valueType)
            || !(GetConcreteTypeToConvert(typeToConvert) is {} concreteTypeToConvert))
            throw new ArgumentException($"Invalid type {typeToConvert}");
        // Clone the incoming options and insert the item converter at the beginning of the clone.
        // Then if converter is actually a JsonConverterFactory (e.g. JsonStringEnumConverter) then the correct JsonConverter<T> will be manufactured or fetched.
        var modifiedOptions = new JsonSerializerOptions(options);
        modifiedOptions.Converters.Insert(0, itemConverter);
        var actualInnerConverter = modifiedOptions.GetConverter(valueType);
        return (JsonConverter)Activator.CreateInstance(typeof(DictionaryValueConverterDecoratorInner<,>).MakeGenericType(new [] { concreteTypeToConvert, valueType }), new object [] { actualInnerConverter })!;
    }
    
    static Type? GetStringKeyedDictionaryValueType(Type typeToConvert)
    {
        if (!(typeToConvert.GetDictionaryKeyValueType() is {} types))
            return null;
        if (types[0] != typeof(string))
            return null;
        return types[1];
    }
    
    static Type? GetConcreteTypeToConvert(Type typeToConvert) =>
        typeToConvert.IsInterface switch
        {
            false when typeToConvert.GetConstructor(Type.EmptyTypes) != null => 
                typeToConvert,
            true when typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() is var generic && (generic == typeof(IDictionary<,>) || generic == typeof(IReadOnlyDictionary<,>)) => 
                typeof(Dictionary<,>).MakeGenericType(typeToConvert.GetGenericArguments()),
            _ => null,
        };
}

internal class DictionaryValueConverterDecoratorInner<TDictionary, TValue> : JsonConverter<TDictionary> where TDictionary : IDictionary<string, TValue>, new()
{
    readonly JsonConverter<TValue> innerConverter;

    public DictionaryValueConverterDecoratorInner(JsonConverter<TValue> innerConverter) => this.innerConverter = innerConverter ?? throw new ArgumentNullException(nameof(innerConverter));
    
    public override TDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 
    {
        if (reader.TokenType == JsonTokenType.Null)
            return (TDictionary?)(object?)null;
        else if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();
        var dictionary = new TDictionary();
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                break;
            else if (reader.TokenType != JsonTokenType.PropertyName)
                throw new JsonException();
            var key = reader.GetString().ThrowOnNull();
            reader.ReadAndAssert();
            var value = innerConverter.Read(ref reader, typeof(TValue), options);
            dictionary.Add(key, value!);
        }
        return dictionary;
    }

    public override void Write(Utf8JsonWriter writer, TDictionary value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        foreach (var pair in value)
        {
            writer.WritePropertyName(pair.Key);
            if (value == null && !innerConverter.HandleNull)
                writer.WriteNullValue();
            else
                innerConverter.Write(writer, pair.Value, options);
        }
        writer.WriteEndObject();
    }
}

public static partial class JsonExtensions
{
    public static ref Utf8JsonReader ReadAndAssert(ref this Utf8JsonReader reader) { if (!reader.Read()) { throw new JsonException(); } return ref reader; }
    public static T ThrowOnNull<T>(this T? value, [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class => value ?? throw new ArgumentNullException(paramName);
}

public static class TypeExtensions
{
    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
        => (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();

    public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
        => type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());

    public static Type []? GetDictionaryKeyValueType(this Type type)
        => type.GetDictionaryKeyValueTypes().SingleOrDefaultIfMultiple();
}

public static class LinqExtensions
{
    // Copied from this answer https://stackoverflow.com/a/25319572
    // By https://stackoverflow.com/users/3542863/sean-rose
    // To https://stackoverflow.com/questions/3185067/singleordefault-throws-an-exception-on-more-than-one-element
    public static TSource? SingleOrDefaultIfMultiple<TSource>(this IEnumerable<TSource> source)
    {
        var elements = source.Take(2).ToArray();
        return (elements.Length == 1) ? elements[0] : default(TSource);
    }
}

然后,假设SaturationJsonConverter是大约JsonConverter<Saturation>,修改你的模型如下:

public partial class Model
{
    //[JsonProperty(PropertyName = "substances", ItemConverterType = typeof(SaturationJsonConverter))]
    [JsonPropertyName("substances"), 
     JsonInclude, 
     JsonConverter(typeof(DictionaryValueConverterDecorator<SaturationJsonConverter>))]
    protected ConcurrentDictionary<string, Saturation> Substances { get; private set; } = new();        
}

并且您的模型应该按要求序列化.演示小提琴#1here.

注:

  • [JsonInclude]是强制非公共属性序列化所必需的.

  • [JsonPropertyName("substances")]相当于Json. NET的JsonPropertyAttribute.PropertyName.

  • 即使启用了JsonObjectCreationHandling.Populate,也不可能使用带有System.Text.Json的自定义转换器来反序列化只读属性,因为现有值没有传递到Read()中.作为一种解决办法,我添加了一个私有setter,如下所示.如果您不想添加私有setter,则可以向模型添加一个参数化构造函数,如下所示,将其标记为[JsonConstructor]:

    public partial class Model
    {
        public Model() { }
    
        [JsonConstructor]
        public Model(ConcurrentDictionary<string, Saturation> substances) => this.Substances = substances ?? throw new ArgumentNullException(nameof(substances));
    
        //[JsonProperty(PropertyName = "substances", ItemConverterType = typeof(SaturationJsonConverter))]
        [JsonPropertyName("substances"), 
         JsonInclude, 
         JsonConverter(typeof(DictionaryValueConverterDecorator<SaturationJsonConverter>))]
        protected ConcurrentDictionary<string, Saturation> Substances { get; } = new();     
    }
    

    here.第一次见面

Csharp相关问答推荐

在C#中使用in修饰符

将修剪声明放入LINQ中

错误401未授权ASP.NET Core.使用JWT创建身份验证/授权

程序集.加载从exe的异常

为什么将鼠标悬停在DateTimeOffset上只显示Hour值?

C#方法从AJAX调用接收NULL

在C#中,有没有一种方法可以集中定义跨多个方法使用的XML参数描述符?

依赖项注入、工厂方法和处置困境

带有列表参数的表达式树

泛型参数在.NET 8 AOT中没有匹配的批注

源代码生成器:CS8795分部方法';Class1.GetS2(字符串)';必须有实现部分,因为它有可访问性修饰符?

如何在GRPC代码First服务的返回类型上使用多态性?

为什么我不能从我的异步任务方法中返回异步任务方法?

为什么C#/MSBuild会自发地为不同的项目使用不同的输出路径?

Azure函数正在返回值列表,但该列表在Chrome中显示为空

为什么当我try 为玩家角色设置动画时,没有从文件夹中拉出正确的图像?

为什么Swashbakle/Swagger在参数中包含变量名?

为什么我的属性即使没有显式地设置任何[必需]属性,也会显示验证?

如何在C#中抽象Vector256;T<;的逻辑以支持不同的硬件配置?

如何获取我在SQL中输入的值