c# - 我们如何使用具有不同属性名称的 JsonSerializer.DeserializeAsyncEnumerable 从巨大的 json 反序列化为 ProtoBuf

标签 c# json deserialization protocol-buffers system.text.json

我有巨大的 json 文件,所以我使用了下面的代码,实际上它有效。

using (FileStream? fileStream = new FileStream("hugefile.json", FileMode.Open))
{
    IAsyncEnumerable<Person?> people = JsonSerializer.DeserializeAsyncEnumerable<Person?>(fileStream);
    await foreach (Person? person in people)
    {
        Console.WriteLine($"Hello, my name is {person.Name}!");
    }
}

我的问题出在使用 protobuf 生成的 Person 类中。它包含一个名为 TrackingDatas 的属性,并且具有 ProtoMember 属性,您可以在下面看到。但在我巨大的 json 中,属性名称是 TrackingData。我想反序列化它,但没有从 ProtoBuf 类中添加或删除任何内容。有人知道吗?

[global::ProtoBuf.ProtoMember(2, Name = @"TrackingData")]
public global::System.Collections.Generic.List<EntityTrackingActivity> TrackingDatas { get; } = new global::System.Collections.Generic.List<EntityTrackingActivity>();

我尝试了下面的代码来更改属性名称,但它对我不起作用。

public class CustomNamingPolicy : JsonNamingPolicy
{
    private readonly Dictionary<string, string> NameMapping = new Dictionary<string, string>()
    {
        [nameof(OASISLevel2TrackingPacket.EntityTracking.TrackingDatas)] = "TrackingData"
    };

    public override string ConvertName(string name)
    {
        var a = NameMapping.GetValueOrDefault(name, name);

        return a;
    }
}
var options = new JsonSerializerOptions()
                    {
                        PropertyNamingPolicy = new CustomNamingPolicy()
                    };
using (FileStream? fileStream = new FileStream("hugefile.json", FileMode.Open))
{
    IAsyncEnumerable<Person?> people = JsonSerializer.DeserializeAsyncEnumerable<Person?>(fileStream, options);
    await foreach (Person? person in people)
    {
        Console.WriteLine($"Hello, my name is {person.Name}!");
    }
}

最佳答案

这里有两个问题:

  1. 属性名称TrackingDatas与 JSON 名称 "TrackingData" 不匹配,但是你的类型是由 Protobuf 自动生成的,所以你不能轻易修改它。

    您已通过添加 PropertyNamingPolicy 正确修复了此问题重新映射名为 TrackingDatas 的所有属性(所有类型)至"TrackingData" .

  2. 您的收藏属性(property)

    public List<EntityTrackingActivity> TrackingDatas { get; } = new ();
    

    只读,但 System.Text.Json 不支持在 .NET 8 之前反序列化只读集合属性

    有关确认,请参阅 Can System.Text.Json.JsonSerializer serialize collections on a read-only property? What's new in .NET 8: Read-only properties

那么,您有什么选择来解决第二个问题?

首先,您可以反序列化为一些适当的 PersonDTO然后将 DTO 映射到 Person例如,使用 AutoMapper。

其次在 .NET 5 及更高版本中,如果您自动生成 Person类被声明为 partial ,例如:

[global::ProtoBuf.ProtoContract]
public partial class EntityTracking
{
    [global::ProtoBuf.ProtoMember(2, Name = @"TrackingData")]
    public global::System.Collections.Generic.List<EntityTrackingActivity> TrackingDatas { get; } = new global::System.Collections.Generic.List<EntityTrackingActivity>();      
}

[global::ProtoBuf.ProtoContract]
public partial class Person : EntityTracking
{
    [global::ProtoBuf.ProtoMember(1, Name = @"Name")]
    public string? Name { get; set; }
}

[global::ProtoBuf.ProtoContract]
public partial class EntityTrackingActivity
{
    [global::ProtoBuf.ProtoMember(1, Name = @"Id")]
    public int Id { get; set; }
}

您可以添加带有 List<EntityTrackingActivity> trackingDatas 的参数化构造函数参数并用 [JsonConstructor] 标记它像这样:

public partial class Person
{
    public Person() { } // Add parameterless constructor if not already auto-generated by protobuf
    
    [JsonConstructor]
    public Person(List<EntityTrackingActivity> trackingDatas) => this.TrackingDatas.AddRange(trackingDatas ?? throw new ArgumentNullException(nameof(trackingDatas)));
}

现在您将能够反序列化 TrackingDatas属性。

演示 fiddle #1 here .

第三,在 .NET 7 及更高版本中,Microsoft 添加了以编程方式自定义 serialization contract 的功能。 System.Text.Json 为每个 .NET 类型创建。使用此 API,您可以添加 typeInfo modifier将所有 JSON 属性名称映射到 ProtoMemberAttribute.Name 的值,并将合成 setter 添加到仅获取 List<T>特性。这种方法完全避免了以任何方式修改类型的需要。

首先添加以下扩展方法:

public static partial class JsonExtensions
{
    public static Action<JsonTypeInfo> InitializeProtoMemberNames(Type type) => typeInfo => 
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object)
            return;
        if (!type.IsAssignableFrom(typeInfo.Type))
            return;
        // Fix property name(s).
        foreach (var property in typeInfo.Properties)
        {
            // Set the JSON property name to be the same as ProtoMemberAttribute.Name
            var name = property.AttributeProvider?.GetCustomAttributes(typeof(global::ProtoBuf.ProtoMemberAttribute), true)
                .OfType<global::ProtoBuf.ProtoMemberAttribute>()
                .FirstOrDefault()
                ?.Name;
            if (name != null)
                property.Name = name;
        }
    };

    public static Action<JsonTypeInfo> InitializeGetOnlyListSetters(Type type) => typeInfo => 
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object)
            return;
        if (!type.IsAssignableFrom(typeInfo.Type))
            return;
        // Add synthetic list setters.
        foreach (var property in typeInfo.Properties)
        {
            if (property.Get != null && property.Set == null && property.PropertyType.GetListItemType() is {} itemType)
            {
                var method = typeof(JsonExtensions).GetMethod(nameof(JsonExtensions.CreateGetOnlyListPropertySetter),
                                                              BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!;
                var genericMethod = method.MakeGenericMethod(new[] { itemType });
                var setter = genericMethod.Invoke(null, new object[] { property }) as Action<object, object?>;
                property.Set = setter;
            }
        }
    };
    
    static Action<Object,Object?>? CreateGetOnlyListPropertySetter<TItem>(JsonPropertyInfo property)
    {
        if (property.Get == null)
            return null;
        (var getter, var name) = (property.Get, property.Name);
        return (obj, value) =>
        {
            var oldValue = (List<TItem>?)getter(obj);
            var newValue = value as List<TItem>;
            if (newValue == oldValue)
                return;
            else if (oldValue == null)
                throw new JsonException("Cannot populate list ${name} in ${obj}.");
            oldValue.Clear();
            if (newValue != null)
                oldValue.AddRange(newValue);
        };
    }

    static MemberInfo? GetMemberInfo(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo);
    
    static Type? GetListItemType(this Type type) =>
        type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>) ? type.GetGenericArguments()[0] : null;
}

然后反序列化,例如如下:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers = { 
            JsonExtensions.InitializeProtoMemberNames(typeof(Person)), 
            JsonExtensions.InitializeGetOnlyListSetters(typeof(Person)) 
        },
    },
};

await using (FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true))
{
    IAsyncEnumerable<Person?> people = JsonSerializer.DeserializeAsyncEnumerable<Person?>(fileStream, options);
    await foreach (Person? person in people)
    {
        Console.WriteLine($"Hello, my name is \"{person?.Name}\", my tracking data is {JsonSerializer.Serialize(person?.TrackingDatas.Select(t => t.Id))}!");
    }           
}

注释:

  • Asynchronous streams and disposables 中所述await using编写异步代码时应使用语法来处理文件流。

  • 为了真正启用异步FileStream访问、通过 useAsync : trueFileStream构造函数。请参阅docs讨论可能的性能影响。

  • CustomNamingPolicy不再需要这种方法。

演示 fiddle #2 here .

最后,在 .NET 8 及更高版本中,通过设置开箱即用地支持填充预分配的只读集合属性

JsonSerializerOptions.PreferredObjectCreationHandling = 
    JsonObjectCreationHandling.Populate;

因此InitializeGetOnlyListSetters()不再需要,您的代码可以简化,例如如下:

var options = new JsonSerializerOptions
{
    PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,           
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        .WithAddedModifier(JsonExtensions.InitializeProtoMemberNames(typeof(Person))),
};

await using var fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
var people = JsonSerializer.DeserializeAsyncEnumerable<Person?>(fileStream, options);
await foreach (var person in people)
{
    // Code to process each person
}   

有关详细信息,请参阅 Populate initialized properties

演示 fiddle #3 here .

关于c# - 我们如何使用具有不同属性名称的 JsonSerializer.DeserializeAsyncEnumerable 从巨大的 json 反序列化为 ProtoBuf,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76438672/

相关文章:

c# - 如何为具有属性路由的操作定义 Html.BeginForm?

java - 反序列化字符串缓冲区

reactjs - 使用 grpc 流在反序列化响应时失败

java - 解析 JSON 字符串返回未定义

c# - 如何在 Protobuf-Net 中保留一个可为空值的数组?

c# - FedEx express 服务

c# - 使用 phpMyAdmin 从远程计算机连接到本地 MySQL 数据库

c# - Active Directory 显示表格中的所有属性

jquery - 将字符串作为对象传递给 jQuery 函数

json - 在 json.Marshal() 中指定结构格式