C# - 将 属性 路径列表及其值动态转换为 Class 对象
C# - Converting list of property paths along with their values into Class object dynamically
为了演示我的问题,我们假设有 3 个实体:
public class Employee
{
public string Name { get; set; }
public Department Department { get; set; }
public Address Address { get; set; }
}
public class Department
{
public string Id { get; set; }
public string Name { get; set; }
}
public class Address
{
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
还有 属性 个路径及其值的列表:
{
"Name":"Abhishek",
"Deparment.Id":"28699787678679",
"Deparment.Name":"IT",
"Address.City":"SomeCity",
"Address.State":"SomeState",
"Address.ZipCode":"29220"
}
最后,我想使用这些键值对列表生成员工对象。
为了演示我的问题,我在这里使用了一个非常简单的“员工”实体。但是,我需要将 100 个这样的键值对转换成一个复杂的对象,所以我不考虑手动映射每个 属性 的选项。
假设这个复杂实体中的所有属性都是字符串属性。我们如何动态地实现这一目标。
我试图通过循环每个 属性 路径并使用 c# 反射以下面的方式动态设置 属性 值来解决它:
(灵感来自 )
private void SetProperty(string compoundProperty, object target, object value)
{
string[] bits = compoundProperty.Split('.');
PropertyInfo propertyToSet = null;
Type objectType = null;
object tempObject = null;
for (int i = 0; i < bits.Length - 1; i++)
{
if (tempObject == null)
tempObject = target;
propertyToSet = tempObject.GetType().GetProperty(bits[i]);
objectType = propertyToSet.PropertyType;
tempObject = propertyToSet.GetValue(tempObject, null);
if (tempObject == null && objectType != null)
{
tempObject = Activator.CreateInstance(objectType);
}
}
propertyToSet = tempObject.GetType().GetProperty(bits.Last());
if (propertyToSet != null && propertyToSet.CanWrite)
propertyToSet.SetValue(target, value, null);
}
您的序列化格式(带路径命名的平面对象)与您的实际对象格式(具有多个子对象的对象图)完全不同,因此您需要进行某种自定义序列化以解决差异.
需要注意的两个主要选项spring:序列化代理类型或自定义序列化。
序列化代理
定义一个与序列化格式直接相关并具有翻译的类型to/from您的实际对象图:
class EmployeeSer
{
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("Department.Id")]
public string DeptId { get; set; }
[JsonPropertyName("Department.Name")]
public string DeptName { get; set; }
// ... repeat above for all properties ...
public static implicit operator Employee(EmployeeSer source)
=> new Employee
{
Name = source.Name,
Department = new Department
{
Id = source.DeptId,
Name = source.DeptName,
},
Address = new Address
{
// ... address properties ...
}
};
public static implicit operator EmployeeSer(Employee source)
=> new Employee
{
Name = source.Name,
DeptId = source.Department?.Id,
DeptName = source.Department?.Name,
// ... address properties ...
};
}
此类型与您提供的 JSON 格式匹配,可以转换 to/from 您的 Employee
类型。这是完整的 .NET Fiddle 展示它的实际效果。
是的,我知道您有一个复杂的用例,但这是最清晰、最直接的选择。
自定义序列化代码
在某些情况下,custom JsonConverter
implementation 是更好的选择。我觉得它们最多很麻烦,但在高度复杂的情况下,它可以节省大量时间和精力。
您正在寻找的似乎是一种生成 JSON 路径而不是图形的通用方法。这是可行的,但要做到正确需要做很多工作。有大量的边缘情况使得它远没有从外面看起来那么简单,而且速度很慢。
这个想法的核心是遍历对象中的所有属性,检查它们的属性等等,然后递归地重复任何不能写成简单值的属性。
整个事情可以通过 Dictionary<string, object>
使用类似的东西来完成:
static Dictionary<string, object> ObjectToPaths(object o)
{
return GatherInternal(o, new Dictionary<string, object>());
static Dictionary<string, object> GatherInternal(object o, Dictionary<string, object> dict, string path = null)
{
if (o is null)
return dict;
var props =
from p in o.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
where p.GetCustomAttribute<JsonIgnoreAttribute>() is null
let pn = p.GetCustomAttribute<JsonPropertyNameAttribute>()
select
(
Property: p,
JsonPath: $"{(string.IsNullOrEmpty(path) ? String.Empty : (path + "."))}{pn?.Name ?? p.Name}",
Simple: p.PropertyType.IsValueType || p.PropertyType == typeof(string)
);
foreach (var (p,jp,s) in props)
{
var v = p.GetValue(o);
if (v is null)
continue;
if (s)
dict[jp] = v;
else
GatherInternal(v, dict, jp);
}
return dict;
}
}
您可以将该字典直接序列化为您的 JSON 格式。有趣的部分是让它走另一条路。
好吧,那和代码会在任何数量的条件下中断,包括引用循环、集合和 类 应该序列化为简单而不是 string
。它还需要大量额外的工作来处理各种序列化修饰符。
我知道在大型复杂对象图的情况下,这是一个简单的选择,但我真的、真的建议您三思而后行。当每个新的极端情况出现时,您将在未来花费数周或数月的时间来尝试解决此问题。
我肯定会选择@Corey 的答案,这似乎是最简单的。
custom converter 可能会有帮助,但并不简单。
我已经开始做一些相关的事情,但它可能无法正常工作。
public class EmployeeConverter : JsonConverter<Employee>
{
public override bool CanConvert(Type typeToConvert)
{
return base.CanConvert(typeToConvert);
}
public override Employee Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var employee = new Employee();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString();
if (!propertyName.Contains('.'))
{
//forward the reader to access the value
reader.Read();
if (reader.TokenType == JsonTokenType.String)
{
var empProp = typeof(Employee).GetProperty(propertyName);
empProp.SetValue(employee, reader.GetString());
}
}
else
{
//forward the reader to access the value
reader.Read();
var stack = new Stack<object>();
stack.Push(employee);
var properties = propertyName.Split('.');
var i = 0;
//should create the matching object type if not already on the stack
//else peek it and set the property
do
{
var currentType = stack.Peek().GetType().Name;
if (properties[i] != currentType)
{
switch (properties[i])
{
case "Department": { stack.Push(new Department()); break; }
case "Address": { stack.Push(new Address()); break; }
case "Project": { stack.Push(new Project()); break; }
}
}
} while (i < properties.Length - 1);
//stack is filled, can set properties on last in object
var lastpropertyname = properties[properties.Length - 1];
var stackcurrent = stack.Peek();
var currentproperty = stackcurrent.GetType().GetProperty(lastpropertyname);
currentproperty.SetValue(stackcurrent, reader.GetString());
// now build back the hierarchy of objects
var lastobject = stack.Pop();
while(stack.Count > 0)
{
var parentobject = stack.Pop();
var parentobjectprop = parentobject.GetType().GetProperty(lastobject.GetType().Name);
parentobjectprop.SetValue(parentobject, lastobject);
lastobject = parentobject;
}
}
}
}
return employee;
}
为了演示我的问题,我们假设有 3 个实体:
public class Employee
{
public string Name { get; set; }
public Department Department { get; set; }
public Address Address { get; set; }
}
public class Department
{
public string Id { get; set; }
public string Name { get; set; }
}
public class Address
{
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
还有 属性 个路径及其值的列表:
{
"Name":"Abhishek",
"Deparment.Id":"28699787678679",
"Deparment.Name":"IT",
"Address.City":"SomeCity",
"Address.State":"SomeState",
"Address.ZipCode":"29220"
}
最后,我想使用这些键值对列表生成员工对象。 为了演示我的问题,我在这里使用了一个非常简单的“员工”实体。但是,我需要将 100 个这样的键值对转换成一个复杂的对象,所以我不考虑手动映射每个 属性 的选项。
假设这个复杂实体中的所有属性都是字符串属性。我们如何动态地实现这一目标。
我试图通过循环每个 属性 路径并使用 c# 反射以下面的方式动态设置 属性 值来解决它:
(灵感来自 )
private void SetProperty(string compoundProperty, object target, object value)
{
string[] bits = compoundProperty.Split('.');
PropertyInfo propertyToSet = null;
Type objectType = null;
object tempObject = null;
for (int i = 0; i < bits.Length - 1; i++)
{
if (tempObject == null)
tempObject = target;
propertyToSet = tempObject.GetType().GetProperty(bits[i]);
objectType = propertyToSet.PropertyType;
tempObject = propertyToSet.GetValue(tempObject, null);
if (tempObject == null && objectType != null)
{
tempObject = Activator.CreateInstance(objectType);
}
}
propertyToSet = tempObject.GetType().GetProperty(bits.Last());
if (propertyToSet != null && propertyToSet.CanWrite)
propertyToSet.SetValue(target, value, null);
}
您的序列化格式(带路径命名的平面对象)与您的实际对象格式(具有多个子对象的对象图)完全不同,因此您需要进行某种自定义序列化以解决差异.
需要注意的两个主要选项spring:序列化代理类型或自定义序列化。
序列化代理
定义一个与序列化格式直接相关并具有翻译的类型to/from您的实际对象图:
class EmployeeSer
{
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("Department.Id")]
public string DeptId { get; set; }
[JsonPropertyName("Department.Name")]
public string DeptName { get; set; }
// ... repeat above for all properties ...
public static implicit operator Employee(EmployeeSer source)
=> new Employee
{
Name = source.Name,
Department = new Department
{
Id = source.DeptId,
Name = source.DeptName,
},
Address = new Address
{
// ... address properties ...
}
};
public static implicit operator EmployeeSer(Employee source)
=> new Employee
{
Name = source.Name,
DeptId = source.Department?.Id,
DeptName = source.Department?.Name,
// ... address properties ...
};
}
此类型与您提供的 JSON 格式匹配,可以转换 to/from 您的 Employee
类型。这是完整的 .NET Fiddle 展示它的实际效果。
是的,我知道您有一个复杂的用例,但这是最清晰、最直接的选择。
自定义序列化代码
在某些情况下,custom JsonConverter
implementation 是更好的选择。我觉得它们最多很麻烦,但在高度复杂的情况下,它可以节省大量时间和精力。
您正在寻找的似乎是一种生成 JSON 路径而不是图形的通用方法。这是可行的,但要做到正确需要做很多工作。有大量的边缘情况使得它远没有从外面看起来那么简单,而且速度很慢。
这个想法的核心是遍历对象中的所有属性,检查它们的属性等等,然后递归地重复任何不能写成简单值的属性。
整个事情可以通过 Dictionary<string, object>
使用类似的东西来完成:
static Dictionary<string, object> ObjectToPaths(object o)
{
return GatherInternal(o, new Dictionary<string, object>());
static Dictionary<string, object> GatherInternal(object o, Dictionary<string, object> dict, string path = null)
{
if (o is null)
return dict;
var props =
from p in o.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
where p.GetCustomAttribute<JsonIgnoreAttribute>() is null
let pn = p.GetCustomAttribute<JsonPropertyNameAttribute>()
select
(
Property: p,
JsonPath: $"{(string.IsNullOrEmpty(path) ? String.Empty : (path + "."))}{pn?.Name ?? p.Name}",
Simple: p.PropertyType.IsValueType || p.PropertyType == typeof(string)
);
foreach (var (p,jp,s) in props)
{
var v = p.GetValue(o);
if (v is null)
continue;
if (s)
dict[jp] = v;
else
GatherInternal(v, dict, jp);
}
return dict;
}
}
您可以将该字典直接序列化为您的 JSON 格式。有趣的部分是让它走另一条路。
好吧,那和代码会在任何数量的条件下中断,包括引用循环、集合和 类 应该序列化为简单而不是 string
。它还需要大量额外的工作来处理各种序列化修饰符。
我知道在大型复杂对象图的情况下,这是一个简单的选择,但我真的、真的建议您三思而后行。当每个新的极端情况出现时,您将在未来花费数周或数月的时间来尝试解决此问题。
我肯定会选择@Corey 的答案,这似乎是最简单的。
custom converter 可能会有帮助,但并不简单。
我已经开始做一些相关的事情,但它可能无法正常工作。
public class EmployeeConverter : JsonConverter<Employee>
{
public override bool CanConvert(Type typeToConvert)
{
return base.CanConvert(typeToConvert);
}
public override Employee Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var employee = new Employee();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString();
if (!propertyName.Contains('.'))
{
//forward the reader to access the value
reader.Read();
if (reader.TokenType == JsonTokenType.String)
{
var empProp = typeof(Employee).GetProperty(propertyName);
empProp.SetValue(employee, reader.GetString());
}
}
else
{
//forward the reader to access the value
reader.Read();
var stack = new Stack<object>();
stack.Push(employee);
var properties = propertyName.Split('.');
var i = 0;
//should create the matching object type if not already on the stack
//else peek it and set the property
do
{
var currentType = stack.Peek().GetType().Name;
if (properties[i] != currentType)
{
switch (properties[i])
{
case "Department": { stack.Push(new Department()); break; }
case "Address": { stack.Push(new Address()); break; }
case "Project": { stack.Push(new Project()); break; }
}
}
} while (i < properties.Length - 1);
//stack is filled, can set properties on last in object
var lastpropertyname = properties[properties.Length - 1];
var stackcurrent = stack.Peek();
var currentproperty = stackcurrent.GetType().GetProperty(lastpropertyname);
currentproperty.SetValue(stackcurrent, reader.GetString());
// now build back the hierarchy of objects
var lastobject = stack.Pop();
while(stack.Count > 0)
{
var parentobject = stack.Pop();
var parentobjectprop = parentobject.GetType().GetProperty(lastobject.GetType().Name);
parentobjectprop.SetValue(parentobject, lastobject);
lastobject = parentobject;
}
}
}
}
return employee;
}