Search code examples
c#.netreflectionasp.net-apicontroller

Reflection GetProperties on different custom classes and collections


In Web API, I have created a custom MediaTypeFormatter to create JSON output with a specific schema, making standard serialization unsuitable. Not that the context above may be relevant, but I need to convert an object, and also IEnumerable to JSON with a schema I specify.

In pseudo code:

If object is a collection
  foreach item in collection
     Write the name/type of the item (will be same for all)
         foreach property of item
              Write the property name, type, and value
Else
  foreach property of object 
     Write the property name, type, and value

The part I am most interested in is acquiring a class property name/value via reflection.

For example, this sent from a controller:

return new MyPerson { .FirstName = "Bob", .DateOfBirth = Convert.ToDateTime("1979-03-01") }

...would be outputted as (crude example as the JSON is easy to change to craft the necessary schema):

{ "MyPerson" : {"FirstName": {"value": "Bob", "Type": "string"}}, "DateOfBirth": {"value": "1979-03-01", "Type": "date"}}

Likewise, a collection would be iterated to produce a similar output:

return new IEnumerable<Foo>() {
    new Foo() { Param1 = "aaa", Param2 = "bbb" },
    new Foo() { Param1 = "ccc", Param2 = "ddd" }
}

...producing

{ "FooCollection": [ 
    { "Foo" : {"Param1": {"value": "aaa", "Type": "string"}}, {"Param2": {"value": "bbb", "Type": "string"}} },
    { "Foo" : {"Param1": {"value": "ccc", "Type": "string"}}, {"Param2": {"value": "ddd", "Type": "string"}} }
]}

I have tried to digest many other examples (1,2) relating to this challenge, but am struggling to adapt them. This is as far as I've managed to get:

private void WriteStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders)
{
    using (StringWriter _stringWriter = new StringWriter()) {
        if (!(value is ICollection))
        {
            foreach (PropertyInfo p in value.GetProperties())
            {
                _stringWriter.Write(GetPropertyData(p));
            }
        }
        else
        {
            foreach (object o in value)
            {
                foreach (PropertyInfo p in o.GetProperties())
                {
                    _stringWriter.Write(GetPropertyData(p));
                }
            }
        }
        // output omitted for brevity...
    }
}

public function GetPropertyData(PropertyInfo p) {
    return string.Format("{name: '{0}', type: '{1}', value: '{2}'},", 
        p.Name, 
        p.PropertyType.ToString(), 
        p.GetValue(p).ToString())
}

Solution

  • I believe you are approaching your problem in the wrong way. Instead of reinventing the wheel by creating a custom MediaTypeFormatter you should just use the correct models for your objects and let the serializer do the rest.

    An example would be to use extension methods for your purpose:

    public static class JsonExtensions
    {
        public static object CreateModels<T>(this IEnumerable<T> models, string modelName = null)
        {
            modelName = modelName ?? typeof(T).Name+"Collection";
    
            return new Dictionary<string, object>()
            {
                { modelName, models.Select(m => CreateModel(m)) }
            };
        }
    
        public static IDictionary<string, object> CreateModel<T>(this T model, string modelName = null)
        {
            modelName = modelName ?? typeof(T).Name;
    
            return new Dictionary<string, object>()
            {
                { modelName, GetProperties(model) }
            };
        }
    
        private static IDictionary<string, object> GetProperties<T>(T obj)
        {
            var props = typeof(T).GetProperties();
            return props.ToDictionary(p => p.Name, p => (object)new { type = p.PropertyType.ToString(), value = p.GetValue(obj, null).ToString() });
        }
    }
    

    Assuming you are using Json.NET in your project, this is how they are used:

    JsonConvert.SerializeObject(new MyPerson { FirstName = "Bob", DateOfBirth = Convert.ToDateTime("1979-03-01") }.CreateModel());
    

    Outputs (pretty printed):

    {
      "MyPerson": {
        "FirstName": {
          "type": "System.String",
          "value": "Bob"
        },
        "DateOfBirth": {
          "type": "System.DateTime",
          "value": "3\/1\/1979 12:00:00 AM"
        }
      }
    }
    

    While:

    JsonConvert.SerializeObject(new List<Foo>() {
        new Foo() { Param1 = "aaa", Param2 = "bbb" },
        new Foo() { Param1 = "ccc", Param2 = "ddd" }
    }.CreateModels());
    

    Outputs:

    {
      "FooCollection": [
        {
          "Foo": {
            "Param1": {
              "type": "System.String",
              "value": "aaa"
            },
            "Param2": {
              "type": "System.String",
              "value": "bbb"
            }
          }
        },
        {
          "Foo": {
            "Param1": {
              "type": "System.String",
              "value": "ccc"
            },
            "Param2": {
              "type": "System.String",
              "value": "ddd"
            }
          }
        }
      ]
    }
    

    .NET Fiddle Demo HERE

    Note

    I see in your examples you are using string and not System.String as type name for the properties. Is not easy to use the aliased type names instead of the CLR true names, but if it is really necessary you may look at this answer for a way to obtain that.