12

在迁移到 .NET Core 3 时,我已从 Newtonsoft.Json 序列化切换到 System.Text.Json.Serialization。在所有功能中,我想继续使用 JsonPropertyName 属性。

Newtonsoft 版本允许对序列化属性进行排序

[JsonProperty(Order = 1)]
public bool Deleted { get; set; }

[JsonProperty(Order = 2)]
public DateTime DeletedDate { get; set; }

有没有办法在 System.Text.Json.Serialization 中实现相同的目标?

4

3 回答 3

7

虽然此功能未在 .NET Core 中实现,但我们可以通过创建自定义JsonConverter. 有几种方法可以实现这一目标。下面是我想出的实现。

说明 -JsonPropertyOrderConverter处理具有至少一个应用了自定义订单值的属性的类型。对于这些类型中的每一种,它都会创建并缓存一个排序函数,该函数将原始对象转换为ExpandoObject具有按特定顺序设置的属性的对象。ExpandoObject维护属性的顺序,因此可以将其传递回以JsonSerializer进行进一步的序列化。转换器还尊重JsonPropertyNameAttributeJsonPropertyOrderAttribute应用于序列化属性的属性。

请注意,Sorter 函数处理的PropertyInfo对象可能会增加一些额外的延迟。如果性能在您的场景中至关重要,请考虑Function<object, object>基于表达式树实现排序器。

class Program
{
    static void Main(string[] args)
    {
        var test = new Test { Bar = 1, Baz = 2, Foo = 3 };

        // Add JsonPropertyOrderConverter to enable ordering
        var opts = new JsonSerializerOptions();
        opts.Converters.Add(new JsonPropertyOrderConverter());

        var serialized = JsonSerializer.Serialize(test, opts);

        // Outputs: {"Bar":1,"Baz":2,"Foo":3}
        Console.WriteLine(serialized);
    }
}

class Test
{
    [JsonPropertyOrder(1)]
    public int Foo { get; set; }

    [JsonPropertyOrder(-1)]
    public int Bar { get; set; }

    // Default order is 0
    public int Baz { get; set; }

}

/// <summary>
/// Sets a custom serialization order for a property.
/// The default value is 0.
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
sealed class JsonPropertyOrderAttribute : Attribute
{
    public int Order { get; }

    public JsonPropertyOrderAttribute(int order)
    {
        Order = order;
    }
}

/// <summary>
/// For Serialization only.
/// Emits properties in the specified order.
/// </summary>
class JsonPropertyOrderConverter : JsonConverter<object>
{
    delegate ExpandoObject SorterFunc(object value, bool ignoreNullValues);

    private static readonly ConcurrentDictionary<Type, SorterFunc> _sorters
        = new ConcurrentDictionary<Type, SorterFunc>();

    public override bool CanConvert(Type typeToConvert)
    {
        // Converter will not run if there is no custom order applied
        var sorter = _sorters.GetOrAdd(typeToConvert, CreateSorter);
        return sorter != null;
    }

    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotSupportedException();
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        // Resolve the sorter.
        // It must exist here (see CanConvert).
        var sorter = _sorters.GetOrAdd(value.GetType(), CreateSorter);

        // Convert value to an ExpandoObject
        // with a certain property order
        var sortedValue = sorter(value, options.IgnoreNullValues);

        // Serialize the ExpandoObject
        JsonSerializer.Serialize(writer, (IDictionary<string, object>)sortedValue, options);
    }

    private SorterFunc CreateSorter(Type type)
    {
        // Get type properties ordered according to JsonPropertyOrder value
        var sortedProperties = type
            .GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Where(x => x.GetCustomAttribute<JsonIgnoreAttribute>(true) == null)
            .Select(x => new
            {
                Info = x,
                Name = x.GetCustomAttribute<JsonPropertyNameAttribute>(true)?.Name ?? x.Name,
                Order = x.GetCustomAttribute<JsonPropertyOrderAttribute>(true)?.Order ?? 0,
                IsExtensionData = x.GetCustomAttribute<JsonExtensionDataAttribute>(true) != null
            })
            .OrderBy(x => x.Order)
            .ToList();

        // If all properties have the same order,
        // there is no sense in explicit sorting
        if (!sortedProperties.Any(x => x.Order != 0))
        {
            return null;
        }
        
        // Return a function assigning property values
        // to an ExpandoObject in a specified order
        return new SorterFunc((src, ignoreNullValues) =>
        {
            IDictionary<string, object> dst = new ExpandoObject();
           
            var isExtensionDataProcessed = false;

            foreach (var prop in sortedProperties)
            {
                var propValue = prop.Info.GetValue(src);

                if (prop.IsExtensionData)
                {
                    if (propValue is IDictionary extensionData)
                    {
                        if (isExtensionDataProcessed)
                        {
                            throw new InvalidOperationException($"The type '{src.GetType().FullName}' cannot have more than one property that has the attribute '{typeof(JsonExtensionDataAttribute).FullName}'.");
                        }

                        foreach (DictionaryEntry entry in extensionData)
                        {
                            dst.Add((string)entry.Key, entry.Value);
                        }
                    }
                    
                    isExtensionDataProcessed = true;
                }
                else if (!ignoreNullValues || !(propValue is null))
                {
                    dst.Add(prop.Name, propValue);
                }
            }

            return (ExpandoObject)dst;
        });
    }
}
于 2020-02-21T11:58:15.443 回答
2

我最终采用了 2-pass 方法。第一遍是我的普通 json 序列化器,带有所有转换器、pocos 等。第二遍是处理空格/缩进/属性顺序/等的“标准化器”。

有很多极端案例试图通过转换器一次完成此操作。属性不只是通过反射,它们可以隐藏在:

  1. 字典
  2. [JsonExtensionData] 属性
  3. JSonElement
  4. 其他转换器!

编写一个处理所有这些的转换器是非常具有挑战性的。所以我采用了 2-pass 方法。第二遍仅对 JsonElement 和 json 编写器进行操作,因此避免了所有极端情况。

(我们在生产中使用它:https ://github.com/microsoft/PowerApps-Language-Tooling/blob/master/src/PAModel/Utility/JsonNormalizer.cs )


// Write out Json in a normalized sorted order. 
// Orders properties, whitespace/indenting, etc. 
internal class JsonNormalizer
{
    public static string Normalize(string jsonStr)
    {
        using (JsonDocument doc = JsonDocument.Parse(jsonStr))
        {
           return Normalize(doc.RootElement);
        } // free up array pool rent
    }

    public static string Normalize(JsonElement je)
    {
        var ms = new MemoryStream();
        JsonWriterOptions opts = new JsonWriterOptions
        {
            Indented = true,
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };
        using (var writer = new Utf8JsonWriter(ms, opts))
        {
            Write(je, writer);
        }

        var bytes = ms.ToArray();
        var str = Encoding.UTF8.GetString(bytes);
        return str;
    }

    private static void Write(JsonElement je, Utf8JsonWriter writer)
    {
        switch(je.ValueKind)
        {
            case JsonValueKind.Object:
                writer.WriteStartObject();

                // !!! This is where we can order the properties. 
                foreach (JsonProperty x in je.EnumerateObject().OrderBy(prop => prop.Name))
                {
                    writer.WritePropertyName(x.Name);
                    Write(x.Value, writer);
                }

                writer.WriteEndObject();
                break;

                // When normalizing... original msapp arrays can be in any order...
            case JsonValueKind.Array:
                writer.WriteStartArray();
                foreach(JsonElement x in je.EnumerateArray())
                {
                    Write(x, writer);
                }
                writer.WriteEndArray();
                break;

            case JsonValueKind.Number:
                writer.WriteNumberValue(je.GetDouble());
                break;

            case JsonValueKind.String:
                // Escape the string 
                writer.WriteStringValue(je.GetString());
                break;

            case JsonValueKind.Null:
                writer.WriteNullValue();
                break;
                
            case JsonValueKind.True:
                writer.WriteBooleanValue(true);
                break;

            case JsonValueKind.False:
                writer.WriteBooleanValue(false);
                break;                

            default:
                throw new NotImplementedException($"Kind: {je.ValueKind}");

        }
    }
}    
于 2020-08-27T23:16:58.367 回答
1

我认为这里的答案都有助于解决“问题”......这是我一直为我工作的自定义解决方案。

JsonPropertyOrderAttribute在@AndreyCh 答案中找到。
也在这里添加:

/// <summary>
/// Orders a property to be in a specific order when serailizing
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonPropertyOrderAttribute : JsonAttribute
{
   public JsonPropertyOrderAttribute(int order)
   {
      Order = order;
   }
    
   public int Order { get; }
}

但这是我的转换器......处理“读取”也让我可以在我的JsonSerializerOptions.

public class JsonPropertyOrderConverter : JsonConverter<object>
{
    public override bool CanConvert(Type typeToConvert) =>
         typeToConvert.GetProperties().Any(x => x.GetCustomAttribute<JsonPropertyOrderAttribute>(true) != null);

    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var newOptions = new JsonSerializerOptions(options);
        if (newOptions.Converters.Contains(this))
        {
            newOptions.Converters.Remove(this);
        }

        return JsonSerializer.Deserialize(ref reader, typeToConvert, newOptions);
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        var orderedProperites = value.GetType().GetProperties()
            .Where(x => x.GetCustomAttribute<JsonIgnoreAttribute>(true) == null)
            .Select(x => new
            {
                Info = x,
                Order = x.GetCustomAttribute<JsonPropertyOrderAttribute>(true)?.Order ?? 0
            })
            .OrderBy(x => x.Order)
            .Select(x => x.Info);

        var work = new Dictionary<string, object>();
        foreach (var property in orderedProperites)
        {
            if (property.PropertyType.IsClass)
            {
                var propValue = property.GetValue(value, null);
                if (propValue == null && options.IgnoreNullValues)
                {
                    //do nothing
                }
                else
                {
                    var classObj = JsonSerializer.Deserialize<object>(JsonSerializer.Serialize(propValue, options));

                    var jsonPropertyName = property.GetCustomAttribute<JsonPropertyNameAttribute>(true)?.Name;
                    if (!string.IsNullOrEmpty(jsonPropertyName))
                        work[jsonPropertyName] = classObj;
                    else
                        work[options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name] = classObj;
                }
            }
            else
            {
                var propValue = property.GetValue(value, null);
                if (propValue == null && options.IgnoreNullValues)
                {
                    //do nothing
                }
                else
                {
                    var jsonPropertyName = property.GetCustomAttribute<JsonPropertyNameAttribute>(true)?.Name;
                    if (!string.IsNullOrEmpty(jsonPropertyName))
                        work[jsonPropertyName] = propValue;
                    else
                        work[options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name] = propValue;
                }
            }
        }

        var newValue = JsonSerializer.Deserialize<object>(JsonSerializer.Serialize(work));
        JsonSerializer.Serialize(writer, newValue, options);
    }
}
于 2021-04-02T16:00:13.553 回答