0

I am getting some data from api, which is dynamic, and I would like to use reflection to strongly type this data for reuse in my app. I have three level hierarchy of object.

I would like to use reflection to combine specific properties that are of same type into collection.

Having following classes:

Level 1:

public sealed class DynamicData
{
    [JsonProperty(propertyName: "PersonInfo")]
    public DynamicDataPersonInfo PersonInfo { get; set; }

    [JsonProperty(propertyName: "Location")]
    public DynamicDataLocation Location { get; set; }
}

Level 2:

public sealed class DynamicDataPersonInfo
{
    [JsonProperty("title")]
    public string Title { get; set; }

    [JsonProperty("type")]
    public string Type { get; set; }

    [JsonProperty("properties")]
    public DynamicDataPersonInfoProperties Properties { get; set; }

    [JsonProperty("required")]
    public string[] PersonInfoRequired { get; set; }
}

public sealed class DynamicDataLocation
{
    [JsonProperty(propertyName: "title")]
    public string Title { get; set; }

    [JsonProperty(propertyName: "type")]
    public string Type { get; set; }

    [JsonProperty(propertyName: "properties")]
    public DynamicDataLocationProperties Properties { get; set; }

    [JsonProperty(propertyName: "required")]
    public string[] LocationRequired { get; set; }
}

Level 3:

public sealed class DynamicDataLocationProperties
{
    [JsonProperty(propertyName: "BuildingNumber")]
    public DynamicDataFieldInfo BuildingNumber { get; set; }

    [JsonProperty(propertyName: "UnitNumber")]
    public DynamicDataFieldInfo UnitNumber { get; set; }

    [JsonProperty(propertyName: "StreetName")]
    public DynamicDataFieldInfo StreetName { get; set; }

    [JsonProperty(propertyName: "StreetType")]
    public DynamicDataFieldInfo StreetType { get; set; }

    [JsonProperty(propertyName: "City")]
    public DynamicDataFieldInfo City { get; set; }

    [JsonProperty(propertyName: "StateProvinceCode")]
    public DynamicDataFieldInfo StateProvinceCode { get; set; }

    [JsonProperty(propertyName: "PostalCode")]
    public DynamicDataFieldInfo PostalCode { get; set; }
}

public sealed class DynamicDataPersonInfoProperties
{
    [JsonProperty(propertyName: "FirstGivenName")]
    public DynamicDataFieldInfo FirstGivenName { get; set; }

    [JsonProperty(propertyName: "MiddleName")]
    public DynamicDataFieldInfo MiddleName { get; set; }

    [JsonProperty(propertyName: "FirstSurName")]
    public DynamicDataFieldInfo FirstSurName { get; set; }

    [JsonProperty(propertyName: "DayOfBirth")]
    public DynamicDataFieldInfo DayOfBirth { get; set; }

    [JsonProperty(propertyName: "MonthOfBirth")]
    public DynamicDataFieldInfo MonthOfBirth { get; set; }

    [JsonProperty(propertyName: "YearOfBirth")]
    public DynamicDataFieldInfo YearOfBirth { get; set; }
}

Final level:

public sealed class DynamicDataFieldInfo
{
    [JsonProperty(propertyName: "type")]
    public string Type { get; set; }

    [JsonProperty(propertyName: "description")]
    public string Description { get; set; }

    [JsonProperty(propertyName: "label")]
    public string Label { get; set; }
}

I am attempting to make this generic as possible, and using reflection, but for each level I am using foreach loop. You know where this is going, and code looks terrible in my opinion:

private IReadOnlyList<DynamicProperty> CombineProperties(DynamicData deserializedRawData)
{
    List<DynamicProperty> combinedDynamicProperties = new List<DynamicProperty>();

    // dynamic data
    foreach (PropertyInfo propertiyofDynamicData in deserializedRawData.GetType()
        .GetProperties())
    {
        // dynamic data lower level
        foreach (PropertyInfo propertyOfDynamicDataMember in propertiyofDynamicData.GetType()
            .GetProperties()
            .Where(predicate: x => x.Name == "Properties"))
        {
            foreach (PropertyInfo propertyOfFieldInfo in propertyOfDynamicDataMember.GetType()
                .GetProperties())
            {
                DynamicDataFieldInfo dynamicDataFieldInfo = propertyOfFieldInfo.GetValue(propertyOfDynamicDataMember) as DynamicDataFieldInfo;
                combinedDynamicProperties.Add(new DynamicProperty {DynamicDataFieldInfo = dynamicDataFieldInfo, GroupType = FormHelper.GetFormGroupType(propertiyofDynamicData.Name)});
            }
        }
    }

    return combinedDynamicProperties;
}

Is there a way I can avoid this horrible looping?

sensei
  • 7,044
  • 10
  • 57
  • 125
  • I think you can use either 'SelectMany' or use a recursive call. (You can even implement a flat recursive call using a 'Stack' (`stack itemsToVisit`) list in a loop) – Rafael Jun 10 '19 at 18:05
  • Great idea, would you mind elaborate a bit more to solution with Stack? Would help a lot. – sensei Jun 10 '19 at 18:08
  • in this question [this](https://stackoverflow.com/questions/11830174/how-to-flatten-tree-via-linq) Doug Clutter's answer shows how to do it and even include some UnitTests – Rafael Jun 11 '19 at 19:26

1 Answers1

2

So I tried to make a recursive form of your code, but alas there is too much difference between each step in the loop and you make references in the final selected object back to the first loop. Instead, here are three changes I would make:

  • Change the second foreach to a simple lookup function, because you are looking for a single property called "Properties"
  • Fix the object reference in GetValue (previously was referencing a PropertyInfo, but needs to reference a real object)
  • Evaluate FormGroupType only once per top level property

The changes would look like this:

private IReadOnlyList<DynamicProperty> CombineProperties(DynamicData deserializedRawData)
{
    List<DynamicProperty> combinedDynamicProperties = new List<DynamicProperty>();

    // dynamic data
    foreach (PropertyInfo propertiyofDynamicData in deserializedRawData.GetType()
        .GetProperties())
    {
        object formGroup = propertiyofDynamicData.GetValue(deserializedRawData);
        var formGroupType = FormHelper.GetFormGroupType(propertiyofDynamicData.Name);

        // dynamic data lower level
        PropertyInfo propertyOfDynamicDataMember = propertiyofDynamicData.GetType().GetProperty("Properties");
        object propertiesGroup = propertyOfDynamicDataMember.GetValue(formGroup);

        foreach (PropertyInfo propertyOfFieldInfo in propertyOfDynamicDataMember.GetType()
            .GetProperties())
        {
            if (propertyOfFieldInfo.GetValue(propertiesGroup) is DynamicDataFieldInfo dynamicDataFieldInfo)
                combinedDynamicProperties.Add(new DynamicProperty {DynamicDataFieldInfo = dynamicDataFieldInfo, GroupType = formGroupType });
        }
    }

    return combinedDynamicProperties;
}

Edit: On second thought, we can condense it into a fairly neat LINQ expression

private IReadOnlyList<DynamicProperty> CombineProperties(DynamicData deserializedRawData)
{
    return (from formGroupProp in deserializedRawData.GetType().GetProperties()
            let formGroup = formGroupProp.GetValue(deserializedRawData)
            let formGroupType = FormHelper.GetFormGroupType(formGroupProp.Name)
            let properties = formGroupProp.GetType().GetProperty("Properties").GetValue(formGroup)
            from dProp in properties.GetType().GetProperties()
            let fieldInfo = dProp.GetValue(properties) as DynamicDataFieldInfo
            where fieldInfo is DynamicDataFieldInfo
            select new DynamicProperty { DynamicDataFieldInfo = fieldInfo, GroupType = formGroupType }).ToList()
}

Edit 2: By adding reflection extensions we can further simplify the method in question: (Note: put these in a namespace that is only for special situations like this, otherwise they will pollute the auto-completion for any object)

static class ReflectionExtensions
{
    public static IEnumerable<object> AllProperties(this object root)
    {
        return root.GetType().GetProperties().Select((p) => p.GetValue(root));
    }
    public static IEnumerable<T> AllProperties<T>(this object root)
    {
        return root.GetType().GetProperties().Where((p) => p.GetType() == typeof(T)).Select((p) => p.GetValue(root));
    }
    public static object Property(this object root, string propName)
    {
        return root.GetType().GetProperty(propName)?.GetValue(root);
    }
}
private IReadOnlyList<DynamicProperty> CombineProperties(DynamicData deserializedRawData)
{
    return (from formGroup in deserializedRawData.AllProperties()
            let formGroupType = FormHelper.GetFormGroupType(formGroup.GetType().Name)
            let properties = formGroup.Property("Properties")
            from fieldInfo in properties?.AllProperties<DynamicDataFieldInfo>() ?? Enumerable.Empty<PropertyInfo>()
            select new DynamicProperty { DynamicDataFieldInfo = fieldInfo, GroupType = formGroupType }).ToList()
}
Rhaokiel
  • 813
  • 6
  • 17