【问题标题】:C# - Converting list of property paths along with their values into Class object dynamicallyC# - 将属性路径列表及其值动态转换为 Class 对象
【发布时间】:2021-10-20 07:51:08
【问题描述】:

为了演示我的问题,让我们考虑有 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# 反射以以下方式动态设置属性值来解决它:

(灵感来自https://stackoverflow.com/a/12294308/8588207

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

【问题讨论】:

  • 这可能与Json序列化有关吗?在属性上使用属性 [JsonPropertyName("Deparment.Name")] ?
  • @Hazrelle 试过这样做,但没有运气。
  • 也许我们可以提供更多信息以提供更好的帮助。您拥有的更多示例 json 数据以及如何使用您拥有的信息填写 Employee
  • Json 数据 { "Name":"Abhishek", "Deparment.Id":"28699787678679", "Deparment.Name":"IT", "Address.City":"SomeCity", "Address.State":"SomeState", "Address.ZipCode":"29220", "Project.Address.City":"SomeOtherCity", } 转换为下面的Employee对象var employee = new Employee { Name = "Abhishek", Deparment = new Deparment { Id = "28699787678679", Name = "IT" }, Address = new Address { City = "SomeCity", State = "SomeState" }, Project = new Project { Address = new Address { City = "SomeOtherCity" } } }
  • @Hazrelle 这样就够了吗?

标签: c# reflection


【解决方案1】:

您的序列化格式(带有路径命名的平面对象)与您的实际对象格式(具有多个子对象的对象图)完全不同,因此您需要进行某种自定义序列化来解决差异。

我想到了两个主要选项:序列化代理类型或自定义序列化。

序列化代理

定义一个与序列化格式直接相关的类型,并与您的实际对象图进行转换:

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 格式匹配,并且可以与您的 Employee 类型相互转换。这是一个完整的.NET Fiddle,展示了它的实际应用。

是的,我知道你有一个复杂的用例,但这是最清晰和最直接的选择。

自定义序列化代码

在某些情况下,custom JsonConverter implementation 是更好的选择。我发现它们充其量是繁琐的,但在高度复杂的情况下,它可以节省大量时间和精力。

看来您正在寻找的是一种通用方法,用于生成带有路径而不是图形的 JSON。这是可行的,但要做到这一点需要做很多工作。有大量的边缘情况使它远没有从外面看起来那么简单,而且速度很慢。

这个想法的核心是遍历对象中的所有属性,检查它们的属性等等,然后递归地重复任何不能写成简单值的属性。

整个事情可以通过Dictionary&lt;string, object&gt; 使用类似的东西来完成:

    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。计划现在继续进行序列化代理。谢谢。
【解决方案2】:

我肯定会选择@Corey'answer,这似乎是最简单的。

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

【讨论】:

  • 谢谢@Hazrelle。现在计划继续进行序列化代理。谢谢。
  • 我正在为路径 JSON 名称编写一个通用的 JsonConverter,但我陷入了对象构造细节的困境。反射很有趣但很复杂。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-02
  • 1970-01-01
  • 1970-01-01
  • 2016-01-07
相关资源
最近更新 更多