如何使用JsonSerializer从巨大的json反序列化为ProtoBuf,

ldioqlga  于 2023-06-25  发布在  其他
关注(0)|答案(1)|浏览(137)

我有一个巨大的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}!");
    }
}

我的问题在于Person类,它是用protobuf生成的。它包含一个名为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}!");
    }
}
jutyujz0

jutyujz01#

这里有两个问题:
1.属性TrackingDatas的名称与JSON名称"TrackingData"不匹配,但您的类型是由Protobuf自动生成的,因此您不能轻易修改它。
通过添加一个PropertyNamingPolicy,将所有名为TrackingDatas(所有类型)的属性重新Map到"TrackingData",您已经正确地修复了这个问题。
1.您的收藏财产

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

只读,但System.Text.Json不支持反序列化只读集合属性
如需确认,请参见 Can System.Text.Json.JsonSerializer serialize collections on a read-only property?
那么,解决第二个问题的方法是什么?

首先,您可以反序列化到一些合适的PersonDTO,然后使用AutoMapper将DTOMap到Person
其次在.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属性。
演示小提琴#1 here

第三,在.NET 7及更高版本中,Microsoft增加了以编程方式自定义System.Text.Json为每个.NET类型创建的serialization contract的功能。使用此API,您可以添加一个typeInfo modifier,将所有JSON属性名Map到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))}!");
    }           
}

注意事项:

  • 如异步流和一次性工具中所述,在编写异步代码时,应使用await using语法来处置文件流。
  • 为了实际启用异步FileStream访问,将useAsync : true传递给FileStream构造函数。有关可能的性能影响的讨论,请参阅文档。
  • CustomNamingPolicy不再需要这种方法。

演示小提琴#2 here

相关问题