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.