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

.NET Core Serialize Inherited Class Properties -- Not Just Base Properties When Preserving References

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

在此示例中,我们有三个 classes: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; }
}

在 class 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 class 中属性的顺序,但这不是一个选项,而且是一个非常糟糕的选择。谢谢大家。

有一个非常相似的示例(区分 属性 声明类型的两个子类)并且可以按如下方式进行调整:

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");