Cleo
Cleo

Reputation: 1141

ASP.Net Webmethod: how to deserialize parameter with inheritance class?

I have a WebMethod defined in a class marked with [ScriptService] as follows:

[WebMethod(enableSession: false), ScriptMethod(UseHttpGet = false, ResponseFormat = ResponseFormat.Json)]
public static JsonResult ReportApplicationActivity(JsonApplicationActivityReport request, bool isTestUpload)
{ ... }

The parameter of type JsonApplicationActivityReport is an abstract base class and may be one of many (level-1) child classes. Problem: when I call this WebMethod from a WPF client, the method never gets hit and an exception is thrown on client side stating something like "Internal server error (500)".

My configuration in Global.asax:

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.SerializationBinder = new JsonApiUtils.InheritanceSerializationBinder();
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.All;

So I'm trying to tell the JSON Serializer to use a custom SerializationBinder where I resolve the inheritance. It looks like this:

public class InheritanceSerializationBinder : DefaultSerializationBinder
    {
        public override Type BindToType(string assemblyName, string typeName)
        {
            switch (typeName)
            {
                case "JsonMeasurementActivityReport[]": return typeof(JsonMeasurementActivityReport[]);
                case "JsonMeasurementActivityReport": return typeof(JsonMeasurementActivityReport);
                case "JsonSimulationActivityReport[]": return typeof(JsonSimulationActivityReport[]);
                case "JsonSimulationActivityReport": return typeof(JsonSimulationActivityReport);
                default: return base.BindToType(assemblyName, typeName);
            }
        }
    }

But the BindToType() method never gets called during the request. I have also turned on tracing in Web.config and can see that the requests (2 per call, for whatever reason...) arrive at the server, but no error message or anything that helps me is provided in /trace.axd.

Finally, that's what my client code looks like:

JsonApplicationActivityReport request = new JsonApplicationActivityReport () {...};
HttpWebRequest webRequest = GetWebRequest(Endpoints.ReportApplicationActivity);
using (var writer = webRequest.GetRequestStream())
{
    string jsonRequest = JsonConvert.SerializeObject(new { request = request, isTestUpload = isTestUpload }, Formatting.Indented, new JsonSerializerSettings {  TypeNameHandling = TypeNameHandling.All }); //also includes type information when serializing to JSON (required because the server expects an abstract class that has to be deserialized into the actual child activity class)
    byte[] requestData = Encoding.UTF8.GetBytes(jsonRequest);
    writer.Write(requestData, 0, requestData.Length);
}

var webResponse = (HttpWebResponse)webRequest.GetResponse();
var responseStream = new StreamReader(webResponse.GetResponseStream());

As soon as GetResponse() is called, the exception is fired.

Please tell my what I'm missing here :)

Upvotes: 2

Views: 630

Answers (1)

Imantas
Imantas

Reputation: 1662

WebService/ScriptService doesn't respect the serializer settings you set in your Global.asax. It uses System.Web.Script.Serialization.JavaScriptSerializer which is hardcoded into WebServiceData and you don't have much control over that.

However, it allows you registering your custom JavaScript converters which inherit from JavaScriptConverter. To do that, you need to use the system.web.extensions/scripting/webServices/jsonSerialization/converters configuration in your Web.config file.

For example, let's say you have JsonApplicationActivityReportConverter implemented for handling deserialization of JsonApplicationActivityReport class. In that case, the configuration would look like as follows

<configuration>
  <!-- ... -->
  <system.web.extensions>
    <scripting>
      <webServices>
        <jsonSerialization>
          <converters>
            <add name="JsonApplicationActivityReportConverter" type="YourApplicationNamespace.JsonApplicationActivityReportConverter, YourApplicationAssemblyName"/>
          </converters>
        </jsonSerialization>
      </webServices>
    </scripting>
  </system.web.extensions>
</configuration>

However, the downside is that you have to re-invent most of deserialization to handle this properly in your JsonApplicationActivityReportConverter. As a starting point, the class would look something like...

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web.Script.Serialization;

namespace YourApplicationNamespace    {
    public class JsonApplicationActivityReportConverter : JavaScriptConverter
    {
        public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
        {
            var typeName = (string) dictionary["$type"];
            var typeInfo = Type.GetType(typeName);
            var result = Activator.CreateInstance(typeInfo);

            foreach (var property in typeInfo.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (property.CanWrite && dictionary.ContainsKey(property.Name))
                {
                    property.SetValue(result, dictionary[property.Name]);
                }
            }

            return result;
        }

        public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
        {
            // Reverse the process here if the type is to be used the other direction
        }

        public override IEnumerable<Type> SupportedTypes
        {
            get { yield return typeof(JsonApplicationActivityReport); }
        }
    }
}

Upvotes: 1

Related Questions