0

I have a complex object from a C# third-party library.

For explaining purposes my complex object could look something like this:

myobject.Property1
myobject.Property2
myobject.Property3.SubPropertyA
myobject.Property3.SomeMethodX()

Is it possible to configure Json.Net to include in the serialization of myobject also the name of the of the method and its return value?

I want to achieve a JSON like this:

{
 "Property1" : "valueOfProperty1" ,
 "Property2" : "valueOfProperty2" ,
 "Property3" : {
                  "SubPropertyA" : "valueOfSubPropertyA",
                  "SomeMethodX" : {JSON serialization of SomeMethodX's return value}
               }
}

I am thinking on using Reflection to call SomeMethodX by overriding CreateProperty method of the DefaultContractResolver

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
    JsonProperty property = base.CreateProperty(member, memberSerialization);

    MethodInfo getServiceMethod = property.DeclaringType.GetMethod("MethodX");
    MethodInfo genericGetServiceMethod = getServiceMethod.MakeGenericMethod(new Type[] { typeof(SomeMethodXReturnType) });//SomeMethodX is generic

    // To invoke the method I need a reference to the actual object, I don't know if it can be accessed through the available arguments????
    object returnValue = genericGetServiceMethod.Invoke(targetedObject, null);

    //next I guess I have to add the return value as an extra property for further serialization
    //I don't know how

    return property;
}

I mention again that I am trying to serialize an object from a library, and not some custom object.

Because of my lack of experience with Json.Net I don't know if I am on the right track.

dbc
  • 104,963
  • 20
  • 228
  • 340
whitefang1993
  • 1,666
  • 3
  • 16
  • 27
  • Make it your own Object - create a Helper class with a method that accepts the library object and spits out your Object. Serialize that. The reason for this is so you can specify attributes on on the object and have quite a lot of type safety in your serialization. – Terrance00 Feb 26 '18 at 14:38
  • 1
    Why do you care about the method name? You could just add a property and set its value as the return of the method. Wouldn't need to use recursion for that. – jpgrassi Feb 26 '18 at 15:06
  • @jpgrassi My object is provided by a library through a public interface. How can I set a property to that? – whitefang1993 Feb 26 '18 at 16:42

1 Answers1

0

You can do this with a custom ContractResolver that subclasses DefaultContractResolver.CreateProperties and uses reflection to add synthetic a JsonProperty to the list of properties to serialize for each applicable method:

public class IncludeMethodsContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = base.CreateProperties(type, memberSerialization);
        return properties.Concat(this.GetMethodProperties(type, memberSerialization)).ToList();
    }
}

internal static class ContractResolverExtensions
{
    static readonly HashSet<string> objectMethods;

    static ContractResolverExtensions()
    {
        objectMethods = new HashSet<string>(typeof(object).GetMethods().Where(m => IsCallableMethod(m)).Select(m => m.Name));
    }

    static Func<object, object> CreateMethodGetterGeneric<TObject, TValue>(MethodInfo method)
    {
        if (method == null)
            throw new ArgumentNullException();
        var func = (Func<TObject, TValue>)Delegate.CreateDelegate(typeof(Func<TObject, TValue>), method);
        return (o) => func((TObject)o);
    }

    static Func<object, object> CreateMethodGetter(MethodInfo method, Type type)
    {
        var myMethod = typeof(ContractResolverExtensions).GetMethod("CreateMethodGetterGeneric", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
        return (Func<object, object>)myMethod.MakeGenericMethod(new[] { type, method.ReturnType }).Invoke(null, new [] { method });
    }

    static bool IsCallableMethod(MethodInfo m)
    {
        return !m.ContainsGenericParameters && m.GetParameters().Length == 0 && !m.IsAbstract && m.ReturnType != typeof(void) && !m.IsSpecialName;
    }

    public static IEnumerable<JsonProperty> GetMethodProperties(this DefaultContractResolver resolver, Type type, MemberSerialization memberSerialization)
    {
        if (type.IsValueType || memberSerialization == MemberSerialization.Fields || memberSerialization == MemberSerialization.OptIn)
            return Enumerable.Empty<JsonProperty>();
        var query = from m in type.GetMethods()
                    where IsCallableMethod(m)
                    where !objectMethods.Contains(m.Name) // Skip ToString(), GetHashCode() and GetType() and the like
                    let v = new MethodProvider(CreateMethodGetter(m, type))
                    select new JsonProperty
                    {
                        DeclaringType = type, 
                        PropertyType = m.ReturnType,
                        PropertyName = resolver.GetResolvedPropertyName(m.Name),
                        UnderlyingName = m.Name,
                        ValueProvider = v,
                        AttributeProvider = NoAttributeProvider.Instance,
                        Readable = true,
                        Writable = false,
                    };
        return query;
    }
}

class NoAttributeProvider : IAttributeProvider
{
    static NoAttributeProvider() { instance = new NoAttributeProvider(); }

    static readonly NoAttributeProvider instance;

    public static NoAttributeProvider Instance { get { return instance; } }

    public IList<Attribute> GetAttributes(Type attributeType, bool inherit) { return new Attribute[0]; }

    public IList<Attribute> GetAttributes(bool inherit) { return new Attribute[0]; }
}

class MethodProvider : IValueProvider
{
    readonly Func<object, object> methodGetter;

    public MethodProvider(Func<object, object> methodGetter)
    {
        if (methodGetter == null)
            throw new ArgumentNullException();
        this.methodGetter = methodGetter;
    }

    #region IValueProvider Members

    public object GetValue(object target)
    {
        return methodGetter(target);
    }

    public void SetValue(object target, object value)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Then, you would call it like:

var settings = new JsonSerializerSettings
{
    ContractResolver = new IncludeMethodsContractResolver(),
};
var customJson = JsonConvert.SerializeObject(root, Formatting.Indented, settings);

Notes:

  • You wrote, I have to add the return value as an extra property for further serialization. I don't know how. A contract resolver defines the rules for how to serialize a specific type to JSON. As such it doesn't have access to any specific instance of the type, and so cannot access any return value directly.

    Instead, it must create an IValueProvider that gets and sets values when passed such an instance. So, to serialize method values, I had to implement a custom value provider using a delegate manufactured from the MethodInfo of the method.

  • Since the contract resolver makes heavy use of reflection, you may want to cache it for best performance.

  • I suppressed serialization of the three parameterless methods of object, namely ToString(), GetHashCode() and GetType().

Sample working .Net fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340