One parameter to rule them all: Part 2
In my post yesterday, I recommended that Web Service operations should only
have a single input parameter rather than multiple for the sake of versioning.
In the comments of that post, Todd makes some really good points why having
multiple parameters isn't such a bad thing. Rather than responding in comment
form, I thought [since I want to illustrate with code snippets] I'd just
respond with a new entry. The fact that it also makes my new blog look more
active isn't a bad thing either :-)
One of the most important considerations when designing Web
Service endpoints is the message ... the XML on the wire. This is where the
rubber meets the road. Decisions at this point will impact versioning,
interoperability, and other factors in the future. This is why the
WS-* specifications are primarily about what the message should look like rather
than how the message should be processed. The importance on the message is also
a primary motivation behind the contract first development approach Christian details here and Yasser writes about here.
Let's have a look at a simple operation that uses multiple parameters:
(yes, I'm eventually going to get around to my point ;)
[WebService( Namespace="urn:many-param-service" )]
[SoapDocumentService( SoapBindingUse.Literal,
SoapParameterStyle.Bare )]
public class Service : WebService
{
[WebMethod]
public ResponseType GetTemp( string city, string state )
{
return TemperatureLib.Calculate( city, state );
}
}
The input type and input message are represented like this in the WSDL
contract for the operation above:
<wsdl:definitions ...>
<wsdl:types>
<s:schema targetNamespace="urn:many-param-service">
<s:element name="city" type="s:string" />
<s:element name="state" type="s:string" />
...
</s:schema>
</wsdl:types>
<wsdl:message name="GetTempSoapIn">
<wsdl:part name="city" element="tns:city" />
<wsdl:part name="state" element="tns:state" />
</wsdl:message>
...
</wsdl:definitions>
Notice how the city and state parameters are not contained in another
element, but are direct children of the s:schema
element. I'll tell you why this is a problem in a minute, but the
reason it's like that is because the wsdl:part names in the wsdl:message are not "parameter" and I specified SoapParameterStyle.Bare in code (they each impact the
other depending on whether you're developing code-first or wsdl-first).
"Okay Don, so why not just use SoapParameterStyle.Wrapped?" Because then every
node of that wrapper element becomes a parameter - even if you don't want it to
be. Meaning, there's no way to send extra non-parameter metadata to the
operation. Okay, I know I'm getting too vauge here so I'll use an example.
Suppose we want to send version information to the operation. In the example
above, there's nowhere for it to go. "Why would we want to send version
information to the operation?" Because as we version the operation, we
should include version information so it's obvious what changed in each version.
We can't get that information by simply adding new parameters to the method
signature. I'm not going to go into it here (I will in my article) but there is
a approach Doug talks about here
about how to version datatypes that contain this version metadata. By using a
single uber-parameter, we can include the traditional parameters as well as any
metadata we need to send like version infomation. Then, instead of having to
check for nulls, we can switch on the version
metadata.
Just for completeness, this is what the code for a single parameter operation
might look like:
[WebService( Namespace="urn:one-param-service" )]
[SoapDocumentService( SoapBindingUse.Literal,
SoapParameterStyle.Bare )]
public class Service : System.Web.Services.WebService
{
[WebMethod]
public ResponseType GetTemp( RequestType req )
{
ResponseType res = new ResponseType();
res.City = req.City;
res.State = req.State;
return res;
}
}
And the input type and input message in the WSDL contract for the
operation will look like this:
<wsdl:definitions ...>
<wsdl:types>
<s:schema ...
targetNamespace="urn:one-param-service">
<s:element name="req" type="tns:RequestType" />
<s:complexType name="RequestType">
<s:sequence>
<s:element ... name="City" type="s:string" />
<s:element ... name="State" type="s:string" />
</s:sequence>
</s:complexType>
...
</s:schema>
</wsdl:types>
<wsdl:message name="GetTempSoapIn">
<wsdl:part name="req" element="tns:req" />
</wsdl:message>
...
</wsdl:definitions>
A single parameter approach also has a bit to do with style. I don't feel the
style aspect is as menial as how you use spaces in your method signatures
because it adds a considerable level of consistency to them. And, as I
mentioned above, it adds a level of elegancy to operations which consume
metadata that doesn't work well as a traditional parameter.
Todd also states: "Sometimes it may make sense to put the new parameter
on the TempRequest object, but not always. Following your example, if
TempRequest really is Address, it makes sense to put a ZipCode field on the
object rather than as another parameter to GetTemp. However, if ZipCode doesn't
logically fit into TempRequest (ie, TempRequest is not an Address), then don't
put it there."
That's the beauty of using a single parameter. It's not important whether
TempRequest is an Address or not - or if it contains an Address or not. As far
as the contract is concerned, we know it contains everything GetTemp needs to
function. When we use a single parameter it will always make sense when
something new is added. If you want to play with the differences between single
and multiple parameters, I've posted some sample
code here that might save you some time. Happy coding!
Comments
- Anonymous
November 29, 2004
But what about when we want to send a login and password as string parameters to a web service? (just kidding)