.NET Core 序列化继承的类属性——在保留引用时不仅仅是基础属性

Posted

技术标签:

【中文标题】.NET Core 序列化继承的类属性——在保留引用时不仅仅是基础属性【英文标题】:.NET Core Serialize Inherited Class Properties -- Not Just Base Properties When Preserving References 【发布时间】:2021-12-09 12:59:52 【问题描述】:

我们在使用 System.Text.Json.JsonSerializer 进行序列化时遇到问题。

在此示例中,我们有三个类:StoreEmployeeManager。请注意,Manager 继承自 Employee。

public class Employee

    public string Name  get; set; 

    public int Age  get; set; 


public class Manager : Employee

    public int AllowedPersonalDays  get; set; 


public class Store

    public Employee EmployeeOfTheMonth  get; set; 

    public Manager Manager  get; set; 

    public string Name  get; set; 

Store 类中,我们有一个名为EmployeeOfTheMonth 的属性。好吧,举个例子,假设这个属性引用了与Manager 属性相同的对象。因为EmployeeOfTheMonth首先被序列化,所以它只会序列化Employee属性。在序列化Manager 属性时——因为它是第二个并且是同一个对象——它将添加对EmployeeOfTheMonth 的引用。当我们这样做时,我们将丢失附加到Manager 的附加属性,即AllowedPersonalDays。此外,如您所见,它不会反序列化,因为——虽然经理是员工——但员工不是经理。

这是我们的简短示例:

Manager mgr = new Manager()

    Age = 42,
    AllowedPersonalDays = 14,
    Name = "Jane Doe",
;

Store store = new Store()

    EmployeeOfTheMonth = mgr,
    Manager = mgr,
    Name = "ValuMart"
;

System.Text.Json.JsonSerializerOptions options = new System.Text.Json.JsonSerializerOptions();
options.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve;

string serialized = System.Text.Json.JsonSerializer.Serialize<Store>(store, options);
var deserialized = System.Text.Json.JsonSerializer.Deserialize<Store>(serialized, options); // <-- Will through an exception per reasons stated above

如果我们查看变量serialized,这是内容:


  "$id":"1",
  "EmployeeOfTheMonth": 
    "$id":"2",
    "Name":"Jane Doe",
    "Age":42
  ,
  "Manager": 
    "$ref":"2"
  ,
  "Name":"ValuMart"

使用 System.Text.Json.JsonSerializer,我们如何才能让 EmployeeOfTheMonth 正确序列化为 Manager?也就是说,我们需要序列化如下所示:


  "$id":"1",
  "EmployeeOfTheMonth": 
    "$id":"2",
    "Name":"Jane Doe",
    "Age":42,
    "AllowedPersonalDays":14         <-- We need to retain this property even if the EmployeeOfTheMonth is a Manager
  ,
  "Manager": 
    "$ref":"2"
  ,
  "Name":"ValuMart"

我知道我可以调整Store 类中属性的顺序,但这不是一个选项,也是一个非常糟糕的选择。谢谢大家。

【问题讨论】:

我认为您可以使用支持类似类型处理的序列化/反序列化的自定义序列化程序来解决这个问题(使用一些discriminator,而不是实际类型)。 你需要反序列化,还是仅仅序列化? 如果您需要反序列化和序列化,将引用跟踪与自定义转换器相结合的要求是棘手的 - 并且 MSFT 不直接支持,请参阅 [ReferenceHandler.IgnoreCycles 不适用于自定义转换器 #51715 ](github.com/dotnet/runtime/issues/51715)。但是如果你只需要序列化,有一些相当简单的方法可以得到你想要的。 是的,我们需要序列化和反序列化。我想我需要创建一个自定义转换器。谢谢大家的帮助。 This answer by Alexander Sheremetyev to Resolve cycle references of complex type during JSON serialization using System.Text.Json.Serialization.JsonConverter 展示了如何编写一个自定义转换器,该转换器也发出参考信息。这样做需要实现自定义 ReferenceHandler,因为自 .NET 5 起,MSFT 不会将其内部引用处理程序提供给转换器。 【参考方案1】:

documentation on writing custom converters 有一个非常相似的示例(区分属性声明类型的两个子类),可以进行如下调整:

public class EmployeeConverter : JsonConverter<Employee>

    enum TypeDiscriminator
    
        Employee = 1,
        Manager = 2
    

    private static string s_typeDiscriminatorLabel = "$TypeDiscriminator";

    public override bool CanConvert(Type typeToConvert) =>
        typeof(Employee).IsAssignableFrom(typeToConvert);

    public override Employee Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    
        if (reader.TokenType != JsonTokenType.StartObject)
        
            throw new JsonException();
        

        reader.Read();
        if (reader.TokenType != JsonTokenType.PropertyName)
        
            throw new JsonException();
        

        string propertyName = reader.GetString();
        if (propertyName != s_typeDiscriminatorLabel)
        
            throw new JsonException();
        

        reader.Read();
        if (reader.TokenType != JsonTokenType.Number)
        
            throw new JsonException();
        

        // Instantiate type based on type discriminator value
        TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
        Employee employee = typeDiscriminator switch
        
            TypeDiscriminator.Employee => new Employee(),
            TypeDiscriminator.Manager => new Manager(),
            _ => throw new JsonException()
        ;

        while (reader.Read())
        
            if (reader.TokenType == JsonTokenType.EndObject)
            
                return employee;
            

            if (reader.TokenType == JsonTokenType.PropertyName)
            
                propertyName = reader.GetString();
                reader.Read();
                switch (propertyName)
                
                    case "Name":
                        string name = reader.GetString();
                        employee.Name = name;
                        break;
                    case "Age":
                        int age = reader.GetInt32();
                        employee.Age = age;
                        break;
                    case "AllowedPersonalDays":
                        int allowedPersonalDays = reader.GetInt32();
                        if(employee is Manager manager)
                        
                            manager.AllowedPersonalDays = allowedPersonalDays;
                        
                        else
                        
                            throw new JsonException();
                        
                        break;
                
            
        

        throw new JsonException();
    

    public override void Write(
        Utf8JsonWriter writer, Employee person, JsonSerializerOptions options)
    
        writer.WriteStartObject();

        // Write type indicator based on whether the runtime type is Manager
        writer.WriteNumber(s_typeDiscriminatorLabel, (int)(person is Manager ? TypeDiscriminator.Manager : TypeDiscriminator.Employee));

        writer.WriteString("Name", person.Name);
        writer.WriteNumber("Age", person.Age);

        // Write Manager-ony property only if runtime type is Manager
        if(person is Manager manager)
        
            writer.WriteNumber("AllowedPersonalDays", manager.AllowedPersonalDays);
        

        writer.WriteEndObject();
    

添加一个自定义转换器的实例,它应该可以正确反序列化:

options.Converters.Add(new EmployeeConverter());

string serialized = JsonSerializer.Serialize<Store>(store, options);
var deserialized = JsonSerializer.Deserialize<Store>(serialized, options);
string reserialized = JsonSerializer.Serialize<Store>(deserialized, options);

System.Diagnostics.Debug.Assert(serialized == reserialized, "Manager property should be retained");

【讨论】:

这实际上并没有生成所需的"$id""$ref" 属性,不是吗?

以上是关于.NET Core 序列化继承的类属性——在保留引用时不仅仅是基础属性的主要内容,如果未能解决你的问题,请参考以下文章

在.net core 的webapi项目中将对象序列化成json

获取 .NET Core JsonSerializer 以序列化私有成员

设置 Asp.Net Core MVC Json 选项

继承log4.net的类

JsonIgnore 属性不断序列化 ASP.NET Core 3 中的属性

继承和已知类型问题