0

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);
        }
  • May this be related to Json Serialization? using attribute [JsonPropertyName("Deparment.Name")] on the property ? – Hazrelle Oct 20 '21 at 07:59
  • @Hazrelle Tried doing that, but no luck. – Abhishek Kandi Oct 20 '21 at 08:11
  • Maybe with a bit more information we could help better. a bit more of sample json data you have and how to fill a `Employee` with the information you have. – Hazrelle Oct 20 '21 at 08:16
  • Json Data `{ "Name":"Abhishek", "Deparment.Id":"28699787678679", "Deparment.Name":"IT", "Address.City":"SomeCity", "Address.State":"SomeState", "Address.ZipCode":"29220", "Project.Address.City":"SomeOtherCity", }` To be converted to below Employee object `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" } } }` – Abhishek Kandi Oct 20 '21 at 09:15
  • @Hazrelle Would this be sufficient ? – Abhishek Kandi Oct 20 '21 at 09:18
  • Can you update your question with a valid JSON example please? What you've posted is invalid. – Corey Oct 20 '21 at 09:47
  • @Corey Updated the question with valid json. Thanks. – Abhishek Kandi Oct 20 '21 at 10:03
  • I'm updating my answer, hang on. – Corey Oct 20 '21 at 10:23

2 Answers2

3

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.

Corey
  • 15,524
  • 2
  • 35
  • 68
0

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;
            }
Hazrelle
  • 758
  • 5
  • 9
  • Thanks @Hazrelle. Planning to go ahead Serialization Proxy for now. Thank you. – Abhishek Kandi Oct 20 '21 at 10:53
  • I was toying with writing a generalized JsonConverter for pathed JSON names, but I get bogged down in the object construction details. Reflection is fun but complex. – Corey Oct 20 '21 at 22:36