0

I'm basically trying to use reflection to flatten any class into a dictionary so that I can generically use and bind them in Blazor. I then need to be able to create an instance of the class and populate it with the data from the dictionary (which will have been updated by a component).

e.g

public class Order
{
    public Guid Id { get; set; }
    public Customer Customer { get; set; }
    public string Address { get; set; }
    public string Postcode { get; set; }
    public List<string> Test { get; set; }
    public List<Test> Test2 { get; set; }
}

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName => $"{FirstName} {LastName}";
    public Gender Gender { get; set; }
    public List<string> Test { get; set; }
}

Should become:

{
  "Id": "",
  "Customer.FirstName": "",
  "Customer.LastName": "",
  "Customer.Gender": "",
  "Customer.Test": "",
  "Address": "",
  "Postcode": "",
  "Test": "",
  "Test2": ""
}

For some reason when I iterate the properties of the Order class, Test2 is missed. The loop shows the property in the collection when I put a breakpoint, it just seems to skip it. I've never seen this happen before.

Code: https://dotnetfiddle.net/g1qyVQ

I also don't think the current code with handle further nested depth which I would like it to be able to work with any POCO object really.

Also if anyone knows a better way to do what I'm trying, I would love to find an easier way. Thanks

3
  • 5
    Code needs to be in the post. It can be linked for people to try, as well, but the question has to stand on its own. Please edit the code into the post. That said, it's a lot of code. You need to break it down to a minimal reproducible example. Commented Mar 26, 2021 at 17:51
  • it's going to be difficult to round trip the data. Because you're flattening the entire object hierarchy you'll have to account for collisions anywhere in the tree. What's the underlying reason that you need to use POCO objects and then flatten them into a dictionary? Commented Mar 26, 2021 at 20:38
  • See my latest answer for the best explanation I can give to the what/why Commented Mar 27, 2021 at 0:35

2 Answers 2

1

First of all, good job on linking the code sample. Without that, I would have passed by this question in about three seconds. :D

In GetAllProperties(), your entire loop is inside a giant try catch block, where the catch returns the dictionary as it is so far, without checking what the exception is. So if you don't get everything you expect, you've probably hit an error.

Amend the catch block:

catch (Exception ex) { Console.WriteLine(ex.ToString()); return result; }

Now, you can see the problem:

System.ArgumentException: An item with the same key has already been added. Key: Test

Your object has more than one property named "Test," but Keys in a Dictionary must be unique.

Summary: Errors aren't the enemy, they're your best friend. Don't use try / catch to bypass errors. If you do, you may get "mysterious, never seen that happen before!" results.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks I had totally missed that, I have a much simpler version now using a package called JsonFlatten to flatten the object. I've also decided to create a default fully initialized version of the object first so that later I can just assign values.
0

For anyone interested, here is where I'm at now:

https://dotnetfiddle.net/3ORKNs

using JsonFlatten;
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;

namespace RecursiveClassProperties
{
    public static class Program
    {
        static void Main(string[] args)
        {
            var item = CreateDefaultItem(typeof(Order));
            Console.WriteLine(JsonSerializer.Serialize(item, new JsonSerializerOptions { WriteIndented = true }));
            var json = JsonSerializer.Serialize(item);
            var properties = JObject.Parse(json).Flatten();
            Console.WriteLine(JsonSerializer.Serialize(properties, new JsonSerializerOptions { WriteIndented = true }));
            var formProperties = properties.ToDictionary(x => x.Key, x => new FormResponse(string.Empty));
            Console.WriteLine(JsonSerializer.Serialize(formProperties, new JsonSerializerOptions { WriteIndented = true }));
        }

        private static object CreateFormItem(Type type, Dictionary<string, FormResponse> formProperties, object result = null)
        {
            result = CreateDefaultItem(type);
            return result;
        }

        private static object CreateDefaultItem(Type type, object result = null, object nested = null, bool isBase = false)
        {
            void SetProperty(PropertyInfo property, object instance)
            {
                if (property.PropertyType == typeof(string)) property.SetValue(instance, string.Empty);
                if (property.PropertyType.IsEnum) property.SetValue(instance, 0);
                if (property.PropertyType == typeof(Guid)) property.SetValue(instance, Guid.Empty);
            }

            if (result is null)
            {
                result = Activator.CreateInstance(type);
                isBase = true;
            }

            var properties = type.GetProperties();

            foreach (var property in properties)
            {
                if (!Attribute.IsDefined(property, typeof(FormIgnoreAttribute)) && property.GetSetMethod() is not null)
                {
                    if (property.PropertyType == typeof(string) || property.PropertyType.IsEnum || property.PropertyType == typeof(Guid))
                    {
                        if (isBase) SetProperty(property, result);
                        else if (nested is not null && nested.GetType() is not IList && !nested.GetType().IsGenericType) SetProperty(property, nested);
                    }
                    else
                    {
                        var _nested = default(object);
                        if (isBase)
                        {
                            property.SetValue(result, Activator.CreateInstance(property.PropertyType));
                            _nested = property.GetValue(result);
                        }
                        if (nested is not null)
                        {
                            property.SetValue(nested, Activator.CreateInstance(property.PropertyType));
                            _nested = property.GetValue(nested);
                        }
                        CreateDefaultItem(property.PropertyType, result, _nested);
                    }
                }
            }

            return result;
        }
    }

    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public class FormIgnoreAttribute : Attribute { }

    public class FormResponse
    {
        public FormResponse(string value) => Value = value;
        public string Value { get; set; }
    }

    public class Order
    {
        public Guid Id { get; set; }
        public Customer Customer { get; set; }
        public string Address { get; set; }
        public string Postcode { get; set; }
        public Test Test { get; set; }
        public List<Gender> Genders { get; set; }
        public List<string> Tests { get; set; }
    }

    public enum Gender
    {
        Male,
        Female
    }

    public class Test
    {
        public string Value { get; set; }
        public List<Gender> Genders { get; set; }
        public List<string> Tests { get; set; }
    }

    public class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string FullName => $"{FirstName} {LastName}";
        public Gender Gender { get; set; }
        public Test Test { get; set; }
        public List<Gender> Genders { get; set; }
        public List<string> Tests { get; set; }
    }
}

The idea is that I can assign values to formProperties, pass it to CreateFormItem() and get a populated object back. The reason I'm doing this is because I have a Blazor component Table which has a typeparam TItem, basically think of it as Table<TItem> for those unfamiliar with Blazor. The table is then supplied a list of objects which it can then render.

Flattening the object in this way will both allow me to easily display all properties and subproperties of the class in the table, but most importantly bind the input of a "new item" form which will return the new object to a delegate outside of the component (back in normal .NET) to submit to a creation controller (to put it in the DB). The reason having a Dictionary<string, FormResponse> is important is that with a generic type, you aren't able to bind the input of the form to the "model". You are however able to bind the input to a string property of a class, even if it's not a string. Hence FormResponse.Value.

I will next need to have CreateFormItem() return the object with the actual data from the form. Sorry if this is a bit longwinded, couldn't think of a more concise way to explain it.

Thanks :)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.