Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I accept arbitrary JSON objects in my REST WCF service?

Tags:

json

rest

c#

.net

wcf

I want to implement a service method like this:

[OperationContract]
[WebInvoke(RequestFormat = WebMessageFormat.Json, ResponseFormat=WebMessageFormat.Json)]
public void MakeShape(string shape, string color, IDictionary<string, object> moreArgs)
{
    if (shape == "circle")
    {
        MakeCircle(color, moreArgs);
    }
}

My clients POST objects like:

{
    "shape":"circle",
    "color": "blue",    
    "radius": 42,
    "filled":true,
    "annotation": {"date":"1/1/2012", "owner":"George"}
}

At the call to MakeCircle, moreArgs would have 3 entries ("radius", "filled", and a dictionary named "annotation" which contains 2 key-value pairs.)


The best I've got so far is:

//Step 1: get the raw JSON by accepting a Stream with BodyStyle=WebMessageBodyStyle.Bare
[OperationContract]
[WebInvoke(RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle=WebMessageBodyStyle.Bare)]
public void MakeShape(Stream jsonStream)
{
    //Step 2: parse it into a Dictionary with JavaScriptSerializer or JSON.net
    StreamReader reader = new StreamReader(jsonStream);
    JavaScriptSerializer jsSerializer = new JavaScriptSerializer();
    Dictionary<string, object> args = jsSerializer.Deserialize<Dictionary<string,object>>(reader.ReadToEnd());            

    //Step 3: manually lookup and cast the "standard" arguments, and remove them from the Dictionary
    string shape = (string)args["shape"];
    string color = (string)args["color"];            

    //Step 4: make the original call, passing the remaining Dictionary as moreArgs
    MakeShape(shape,color,args);
}

I could live with a solution like this except step 3 will be a pain to keep in sync across dozens of methods. Obviously something has to open the dictionary and use the extra arguments but I'd rather keep that code out of my communications layer. IMO it goes inside the business logic that knows about the arguments (in this case represented by MakeCircle).

I really like WCF's automatic binding because it eliminates these error-prone manual translations. I wish there were a way to use it for almost everything, except specify a little extra logic for the arguments it doesn't know how to map. Perhaps there's some sort of service behavior that says "pass them to [this code] and I'll deal with it"?


I've considered the "round-tripping" support offered by IExtensibleDataObject, but it doesn't seem to give my code access to the unknown properties - they're wrapped up for the sole purpose of sending back to the client. http://msdn.microsoft.com/en-us/library/ms731083.aspx


Another option would be to use a custom class that contains a IDictionary, and somehow take over the deserialization myself. So the service method would be: [OperationContract] [WebInvoke(RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest)] public void MakeShape(string shape, string color, MoreArgs moreArgs)

And I'd have to force clients into a stricter structure:

{
    "shape":"circle",
    "color": "blue",
    "moreArgs":{
        "radius": 42,
        "filled":true
        "annotation": {"date":"1/1/2012", "owner":"George"}
        }
}

That's not ideal, but I could live with it. The question becomes how to define MoreArgs and get one populated properly. My next try:

[DataContract]
public class MoreArgs : ISerializable
{
    public Dictionary<string, object> Properties;
    public MoreArgs(SerializationInfo info, StreamingContext context)  
    {
        Properties = new Dictionary<string, object>();  
        foreach (var entry in info)  
        {                      
        Properties.Add(entry.Name, entry.Value);  
        }
    }  
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        foreach (string key in Properties.Keys)
        {
        info.AddValue(key, Properties[key]);
        }  
    }
}

This throws an InvalidDataContractException on service start (... MoreArgs' cannot be ISerializable and have DataContractAttribute attribute.)

Removing the [DataContract] attribute throws the InvalidDataContractException I expect (...MoreArgs' cannot be serialized. Consider marking it with the DataContractAttribute attribute...).

Also as expected, removing the ISerializable inheritance clears the exception, but results in moreArgs.Properties being null at the call to MakeCircle.


I also wonder if there's some hybrid solution I can use? Perhaps:

  • Accept a stream and construct the argument dictionary as in my first attempt
  • Define the method with a MoreArgs argument like my later attempt
  • Populate a MoreArgs object from the dictionary pulled from the stream
  • Somehow re-call WCF, saying "invoke the method that would be called if you had these arguments" (specifying the original argument dictionary, plus the new properly-populated MoreArgs).

MoreArgs would then contain the original arguments too, but that's probably not a disaster. I think I could probably make the call I need using reflection, but that feels silly when WCF must have this function internally, debugged and optimized to boot.

like image 353
solublefish Avatar asked Jul 13 '12 07:07

solublefish


1 Answers

@Melissa Avery-Weir

I'm not happy with my "solution" but I had to move forward.

At app startup, for every method I want to call, I stuff a MethodInfo in a lookup table. It's keyed by an interface and method name. I use reflection and a custom attribute to find them, but any number of techniques could work here.

My one WCF service method accepts an interface name, a method name, and a Stream as arguments. I use JSON.NET to deserialize arguments to a Dictionary and pass that to my dispatcher.

The dispatcher looks up the MethodInfo by interface and method name. Then, matching arguments from the dictionary to parameters in the MethodInfo, I fill an argument array. If the target method actually has a Dictionary moreArgs parameter, it gets any unmatched arguments. Finally I call MethodInfo.Invoke, passing the freshly populated argument array.

It's a lot of fiddly code to do some things that WCF almost does for me, but I didn't find a better solution.

There are some benefits of controlling all this myself. My favorite is the ability to use the saved MethodInfos to automatically generate client-side call stubs in whatever language I want.

If the late binding turns out to be a performance issue I'll consider putting all the calls manually in a big switch(methodName). If my interfaces are still changing frequently, I may try to generate that boilerplate binding code as a build step. Probably I'll never bother since I'll be bottlenecked on DB access.

like image 133
solublefish Avatar answered Oct 19 '22 23:10

solublefish