SOA Made Real
May 18, 2005
In my previous
column I presented a style of writing WSDL files that, when followed, resulted in
reusable components and a nice service-oriented architecture. Pedants may complain
that I
was essentially proposing rpc/literal
rather than doc/literal
by
requiring that every request and response had a name determined algorithmically from
the
operation name. There are two responses to this: first, since you can have attributes
it's
not RPC; second, who cares?
In this column we'll actually build a web service using the style, as well as the examples, from the previous column. Again, we'll use Norm Walsh's where in the world service. For grins, we'll look at a C#/.NET client and a Python server.
Getting Started
The .NET stub generator is called, interestingly enough, wsdl
. The first step
is to stitch together the fragments from last time and build a single WSDL file that
we can
feed into the wsdl
program. The only thing missing is the schema definition. As
I first discussed in September of 2003, use the "Russian Doll" pattern, which eschews the XSD
type system for inlining definitions. Also, reuse structures based on element name,
not
type.
Looking at the nearby
operation, we see the following "calling
sequence":
nearby( xs:string userid, xs:string units = "mi", xs:decimal distance = 50.0)
This isn't a C++-like description, since either or both of userid
or
distance
can be omitted. This description, using the above style, results in
the following schema declaration:
<xsd:element name="nearby"> <xsd:complexType> <xsd:sequence> <xsd:element name="userid" type="xsd:string"/> <!-- units defaults to "mi" --> <xsd:element name="units" type="xsd:string" minOccurs="0"/> <!-- distance defaults to "50" --> <xsd:element name="distance" type="xsd:decimal" minOccurs="0"/> </xsd:sequence> </xsd:complexType> </xsd:element>
This is, admittedly, verbose, with nine lines of definition. The return value, which is an array of a complex type, is almost twice as long, and pretty heavy on the XSD-specific overhead:
<xsd:element name="nearby-response"> <xsd:complexType> <xsd:sequence> <xsd:element name="landmark"> <xsd:complexType> <xsd:sequence> <xsd:element name="name" type="xsd:string"/> <xsd:element name="latitude" type="xsd:float"/> <xsd:element name="longitude" type="xsd:float"/> <xsd:element name="distance" type="xsd:decimal"/> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element>
Just looking at those first half-dozen lines is enough to make you think of the Monty
Python "spam" sketch. In defense, I can only point out that it's pretty
boilerplate, and that you can pull out the landmark
definition into its own
element declaration, and use the ref
attribute:
<xsd:element ref="tns:landmark" minOccurs="0" maxOccurs="unbounded"/>
Which method you use is a matter of taste. I typically inline the element if it's only used once.
Putting it all together, a complete WSDL can be found in listing 1.
Simple, elegant, unsupported
So if we feed our first WSDL into wsdl
, we get
Error: Unable to import binding '&COMPONENT;-binding' from namespace '&BASE;'. - Unable to import operation 'nearby-operation'. - The element '&BASE;:nearby' is missing.
It turns out that my nice little scheme to use DTD's and ENTITY declaration, so that
I only
have to write things like the namespace URI once, and so that I can make a skeleton
WSDL and
edit a few lines at the beginning before filling in the details...isn't supported.
In fact,
just having a DOCTYPE with a few entities (even if they're never used) gives the .NET
wsdl
tools fits
I don't recall the WS-I profile saying you shouldn't use a DTD in a WSDL file.
So we have to edit the code from listing
1 and replace the entities with their values. If you do that, wsdl
does
the right thing, generating a C# client stub. The stub turns the "where in the
world" service into a class and each operation becomes a method, which is pretty
standard (although it's pretty nice when coupled with VisualStudio's auto-completion
features).
Since our operations all take a sequence, wsdl
also creates an object for each
input and output message. Putting them together, we can write code like this:
witw b = new witw(); b.Url = "http://os360.datapower.com:4444/test/server"; @is i = new @is(); i.userid = "rsalz"; isresponse ir = b.isoperation(i);
Notice how you can change where the server is by setting the Url
property on
the binding, b
. Also note that wsdl
appends response or
operation to distinguish among request data, response value, and operation.
For those who don't know C#, the atsign is a token-level quoting character. In this
case
is is a C# keyword, so you have to write it as @is
. You will almost
definitely face this kind of issue when creating your own services -- no doubt you'll
end up
using an identifier that's a keyword in some language, somewhere. Hopefully the WSDL
toolkit(s) for the language in conflict will have a mapping mechanism. If not, or
if it's
too ugly (by some metric), you'll have to edit your service description. Using mixedCase
or
under_scores will probably avoid that kind of problem.
Messy, messy
Let's look at the atlandmark
operation. Its allows a user to specify that they
are at a particular landmark. (For example, is everyone in your dinner party over
at Fry's
Electronics?) The function definition is this:
atlandmark( xs:string userid, xs:string landmark)
But this is what you end up having to write as code:
atlandmark l = new atlandmark(); l.landmark = "Fry's Electronics"; l.userid = "rsalz"; b.atlandmarkoperation(l);
That's a hell of a lot more ugly than the obvious:
b.atlandmark("Fry's Electronics", "rsalz");
More from Rich Salz |
What happened? Unfortunately, it's fallout from the way I wrote the WSDL. Every operation has a single input parameter and a single output parameter. That means your language bindings have to create "objects", or at least containers, to hold what would be the bits of XML on the wire. That's kind of ugly, but it's the dirty little secret of SOA: your system will be better off, but you will have to write more code.
If we compile and run the code (available in listing 2), and capture what it sends, we'll see that things make sense (I pretty-printed the message; it was sent as a single long line):
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <atlandmark xmlns="http://www.example.org/witw"> <userid xmlns="">rsalz</userid> <landmark xmlns="">Tidepool</landmark> </atlandmark> </soap:Body> </soap:Envelope>
Sure enough, we're shipping around a clean bit of XML. Granted, there are spurious (and redundant) namespace declarations there, but that's a small nit. (Bonus points if you can figure out which attribute in the schema definition caused the innermost empty namespace declarations.) And look, it's SOAP 1.1!
Next time we'll look at the server and discuss the joys of method dispatch.