Home > Enterprise >  C# - Converting list of property paths along with their values into Class object dynamically
C# - Converting list of property paths along with their values into Class object dynamically

Time:10-21

For demonstrating my problem, let's consider there are 3 entities:

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

And there are list of property paths along with their values:

{
    "Name":"Abhishek",
    "Deparment.Id":"28699787678679",
    "Deparment.Name":"IT",
    "Address.City":"SomeCity",
    "Address.State":"SomeState",
    "Address.ZipCode":"29220"
}

Finally I would like to generate the employee object using these list of key value pairs. To demonstrate my problem, I've used a very simple "Employee" entity here. However, I need to convert 100s of such key value pairs into one complex object & so I'm not considering option to mapping each property manually.

Provided all the properties in this complex entity are string properties. How can we achieve this dynamically.

I've tried to solve it by looping each property path & setting the property value dynamically in below manner using c# reflection :

(Inspired from 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);
        }

CodePudding user response:

Your serialization format (flat object with path naming) is quite different from your actual object format (object graph with multiple sub-objects), so you'll need to do some sort of custom serialization to account for the difference.

Two main options spring to mind: serialization proxy type or custom serialization.

Serialization Proxy

Define a type which directly correlates to the serialization format and has translation to/from your actual object graph:

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

This type matches the JSON format you supplied and can convert to/from your Employee type. Here's a full .NET Fiddle showing it in action.

And yes, I know you've got a complex use-case, but this is the clearest and most direct option.

Custom Serialization Code

In some cases a custom JsonConverter implementation is the better way to go. I find them cumbersome at best, but in high complexity cases it can save a lot of time and effort.

It appears that what you're looking for is a general-purpose method for generating JSON with paths instead of graphs. It's doable, but it's a lot of work to get right. There are a ton of edge cases that make it nowhere near as simple as it seems from the outside, and it's slow.

The core of the idea is to iterate through all properties in the object, checking their attributes and so forth, then repeat recursively on any properties that can't be written as simple values.

The whole thing can be done via a Dictionary<string, object> using something like:

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

You can serialize that dictionary directly to your JSON format. The fun part is getting it to go the other way.

Well, that and the code will break on any number of conditions, including reference loops, collections and classes that should be serialized as simple other than string. It needs a ton of additional work to handle various serialization modifiers as well.

I know that it feels like this is a simple option to take in the case of a large and complex object graph, but I'd really, really suggest you think twice about this. You're going to spend weeks or months in the future trying to fix this when each new edge case comes up.

CodePudding user response:

I would definitively go with @Corey'answer which seems the easiest.

A custom converter may help but it's not straightforward.

I've begin something related, but it's probably not working as is.

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