.NET Core 序列化继承的 Class 属性——在保留引用时不仅仅是基本属性
.NET Core Serialize Inherited Class Properties -- Not Just Base Properties When Preserving References
我们在使用 System.Text.Json.JsonSerializer 序列化时遇到问题。
在此示例中,我们有三个 classes:Store
、Employee
和 Manager
。需要注意的是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");
我们在使用 System.Text.Json.JsonSerializer 序列化时遇到问题。
在此示例中,我们有三个 classes:Store
、Employee
和 Manager
。需要注意的是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");