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;
            }