You can make a difference on this day in April. Go and sign this petition.
Help save IDL.
What I personally would love to see are two new switched alongside midl.exe
/mktyplib203:
/wsdl – Verifies that all attributes used are applicable to a wsdl
contract and then generates a *.wsdl file from IDL
/serviceModel – Verifies that all attributes are System.ServiceModel
compatible and then generates a *.cpp file (managed code!) definition Indigo
[ServiceContract] and [OperationContract].
The Indigo bits are out at MSDN Subscriber downloads. Go get them and start
playing.
Tools, SDKs and DDKs,
Platform Tools, SDKs, DDKs
WinFX SDK –
Community Technology Preview
Avalon and Indigo Community Technology Preview - March 05 (English)
I’ll write a few more parts of my little
Indigo series next weekend (too busy during the week), and will move from “throw
arbitrary XML on the wire” to typed messages. However, before I’ll
do so, I am curious about your opinion and I am asking you to comment (on the
blog-site) on which of the following two declarations you would prefer.
I should probably quickly explain a few things before I let you look at the
code snippets: [DataContract] attribute essentially replaces [Serializable]
for Indigo and is used to label classes than can be serialized by the System.Runtime.Serialization
infrastructure into XML or into a binary representation. So the
serialization control through attributes is unified and independent of the
actual output flavor you choose at runtime. The [DataMember] attribute labels
fields or properties that are part of the data contract and should be (de)serialized.
Unlike the current serialization models of Remoting (System.Runtime.Remoting.Formatters)
and the XML Serializer (System.Xml.Serialization) where the
serializers grab anything public, this model is strictly opt-in, meaning
that public fields and properties do not get serialized unless you explicitly
label them with [DataMember]. Even more surprising, the new
serialization infrastructure does work with fields that are private.
I have a clear preference for one of these two declarations and have also what
I think to be a solid explanation for why I prefer it, but before I elaborate,
I am interested in your opinion.
Version A
|
[DataContract]
public partial
class Address
{
[DataMember("Company")]
private string company;
[DataMember("RecipientName")]
private string recipientName;
[DataMember("AddressLine1")]
private string addressLine1;
... more fields ...
public string Company
{
get
{ return company; }
set
{ company = value; }
}
public string RecipientName
{
get
{ return recipientName; }
set
{ recipientName = value; }
}
public string AddressLine1
{
get
{ return addressLine1; }
set
{ addressLine1 = value; }
}
... more properties and methods and stuff ...
}
|
Version B
|
[DataContract]
public partial
class Address
{
private string company;
private string recipientName;
private string addressLine1;
... more fields ...
[DataMember("Company")]
public string Company
{
get
{ return company; }
set
{ company = value; }
}
[DataMember("RecipientName")]
public string RecipientName
{
get
{ return recipientName; }
set
{ recipientName = value; }
}
[DataMember("AddressLine1")]
public string AddressLine1
{
get
{ return addressLine1; }
set
{ addressLine1 = value; }
}
... more properties and methods and stuff ...
}
|
Consider this obvious statement: The class is declared in this way to provide
programmatic access to and encapsulation of data that will eventually be
serialized into some wire format or deserialized from a wire format.
Christian Weyer is staying at my
place for the next three nights, because we’re both presenting at a
Microsoft Visual Studio 2005 training at Microsoft’s Neuss office, which
is more or less down the street (highway) from where I live. Christian brought
some good beer from his region (Franken – Bavaria’s northern part)
and we’re having some of that, watch some TV (“We Were
Soldiers” and “Broken Arrow”, we’re just guys like the
next one), and otherwise get some email done, and chat. We just agreed on our
programming hero. The prize goes to: Lutz
Roeder. We’d be nothing without Reflector.
[Read Part
1 and Part
2 first]
Like with parts 1 and 2, I’ll stick with the “this isn’t
RPC” theme for this 3rd part of this little series and will show
how to flow free form XML from and to services. However, I will drop the “client”/”server”
nomenclature from here on and will talk about endpoints. If you look at the
contract below (along with the following explanation, of course), you’ll
quickly figure out why – both parties in the “buyer”/”seller”
conversation I am declaring in the contract below, act as client and as server at
the same time.
In contrast to the previous two examples, I am not using the raw Message
class, but I move one notch up on the messaging stack and use the XmlSerializer
formatting mode for Indigo, which allows me to flow the contents of an XmlNode
between services just like it can be done today with ASP.NET Web Services. In
addition, I show how custom message headers can be declared and flowed with (really:
inside) messages. But first things first:
The snippet below declares one contract (!) with two endpoint service
contracts. One endpoint defines the “seller” side and the other
defines the “buyer” side of a duplex conversation that two service
implementations will have about a (simplified) purchasing process. It also
defines an application-specific (SOAP-) header that is used to flow the
purchasing process identifier between the parties. That identifier can be used
to locate the process state from disk or from some in-memory location at either
side as the conversation progresses.
The seller-side service contract is defined through the ISeller interface
that is appropriately labeled with a [ServiceContract] attribute and the
buyer-side likewise defined through the IBuyer interface. The fusion of
these two interfaces into what is effectively a single contract is established
by mutually linking both interfaces by setting the respective CallbackContract
property of the [ServiceContract] attribute to the respective other
interface type. I highlighted the two places where that’s being done.
When I say “one contract”, that is not really true on the WSDL level.
In WSDL, both interfaces would indeed be represented as independent contracts.
(Which goes to show that WSDL isn’t really “the contract”,
but represents just a subset of the complete metadata model).
Each operation in these contracts is labeled with an [OperationContract]
attribute that defines the message flow as IsOneWay=true. That’s
so because in a duplex conversation, messages flow always unidirectionally and the
receiver answers not by “returning a result”, but rather by sending
a message (or multiple messages) to the other party’s endpoint. All
operation contracts also define the operation style to be DocumentBare,
which means that the infrastructure will not auto-generate body wrapper elements.
Instead, each operation defines its own body wrapper by flagging the XmlNode
typed argument for the message content with a [MessageBody] attribute
and assigning an appropriate name to it. Above the XmlNode content
argument, you can see how the custom header PurchaseProcessHeader is specified
for each operation. Custom headers are flagged with the [MessageHeader]
attribute and therefore flow in the soap:Header section of the message.
|
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Runtime.Serialization;
using System.Xml;
using System.Xml.Serialization;
namespace DuplexMessagingConversation
{
[XmlRoot(Namespace = PurchaseProcessHeader.NamespaceURI)]
[XmlType(Namespace = PurchaseProcessHeader.NamespaceURI)]
public class PurchaseProcessHeader
{
public
const string
NamespaceURI="urn:newtelligence-com:indigosamples:purchasing";
public
const string
ElementName="PurchaseOrder";
private
string orderIdentifier;
public
string OrderIdentifier
{
get { return
orderIdentifier; }
set { orderIdentifier = value;
}
}
}
[ServiceContract(Namespace
= "urn:newtelligence-com:indigosamples:seller",
Session = false,
CallbackContract = typeof(IBuyer),
FormatMode = ContractFormatMode.XmlSerializer)]
interface ISeller
{
[OperationContract(IsOneWay=true,IsInitiating=true,
Style=ServiceOperationStyle.DocumentBare)]
void
HandlePurchaseOrder(
[MessageHeader(Name=PurchaseProcessHeader.ElementName,
Namespace=PurchaseProcessHeader.NamespaceURI)]
PurchaseProcessHeader process,
[MessageBody(Name="PurchaseOrderMessage")]
XmlNode purchaseOrder);
[OperationContract(IsOneWay
= true, IsInitiating = false,
Style = ServiceOperationStyle.DocumentBare)]
void
HandlePaymentNotification(
[MessageHeader(Name = PurchaseProcessHeader.ElementName,
Namespace = PurchaseProcessHeader.NamespaceURI)]
PurchaseProcessHeader process,
[MessageBody(Name =
"PaymentNotificationMessage")]
XmlNode paymentNotification);
[OperationContract(IsOneWay
= true, IsInitiating = false, IsTerminating = true,
Style = ServiceOperationStyle.DocumentBare)]
void
HandleShippingConfirmation(
[MessageHeader(Name = PurchaseProcessHeader.ElementName,
Namespace = PurchaseProcessHeader.NamespaceURI)]
PurchaseProcessHeader process,
[MessageBody(Name =
"ShippingConfirmationMessage")]
XmlNode shippingConfirmation);
}
[ServiceContract(Namespace="urn:newtelligence-com:indigosamples:buyer",
Session = false,
CallbackContract = typeof(ISeller),
FormatMode=ContractFormatMode.XmlSerializer)]
interface IBuyer
{
[OperationContract(IsOneWay
= true, IsInitiating = true,
Style = ServiceOperationStyle.DocumentBare)]
void
HandlePurchaseOrderConfirmation(
[MessageHeader(Name = PurchaseProcessHeader.ElementName,
Namespace = PurchaseProcessHeader.NamespaceURI)]
PurchaseProcessHeader process,
[MessageBody(Name =
"PurchaseOrderConfirmationMessage")]
XmlNode purchaseOrderConfirmation);
[OperationContract(IsOneWay
= true, IsInitiating = false,
Style = ServiceOperationStyle.DocumentBare)]
void
HandleInvoice(
[MessageHeader(Name = PurchaseProcessHeader.ElementName,
Namespace = PurchaseProcessHeader.NamespaceURI)]
PurchaseProcessHeader process,
[MessageBody(Name = "InvoiceMessage")]
XmlNode invoice);
[OperationContract(IsOneWay
= true, IsInitiating = false, IsTerminating = true,
Style = ServiceOperationStyle.DocumentBare)]
void
HandleShippingNotification(
[MessageHeader(Name = PurchaseProcessHeader.ElementName,
Namespace = PurchaseProcessHeader.NamespaceURI)]
PurchaseProcessHeader process,
[MessageBody(Name =
"ShippingNotificationMessage")]
XmlNode shippingNotification);
}
}
|
To illustrate the effect of these declarations on the wire (I will spare you
the XSD/WSDL goop), I’ll show an sample message (grabbed from the
debugger) as it can be seen at the ISeller endpoint’s HandlePurchaseOrder
operation when it arrives.
|
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:r="http://schemas.xmlsoap.org/ws/2005/01/rm">
<s:Header>
<a:Action s:mustUnderstand="1">
urn:newtelligence-com:indigosamples:seller/ISeller/HandlePurchaseOrder
</a:Action>
<h:PurchaseOrder xmlns="urn:newtelligence-com:indigosamples:purchasing"
xmlns:h="urn:newtelligence-com:indigosamples:purchasing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<OrderIdentifier>1234567890</OrderIdentifier>
</h:PurchaseOrder>
<r:Sequence s:mustUnderstand="1">
<r:Identifier>uuid:b99041bf-fab8-45dd-9235-0909d9c61d04;id=2</r:Identifier>
<r:MessageNumber>1</r:MessageNumber>
</r:Sequence>
<a:From>
<a:Address>net.tcp://localhost/buyer/reply/e01289a8-424f-4e1a-bba5-b3fb7c92a023</a:Address>
</a:From>
<a:To s:mustUnderstand="1">net.tcp://localhost/seller</a:To>
</s:Header>
<s:Body>
<PurchaseOrderMessage xmlns="urn:newtelligence-com:indigosamples:seller">
<Order xmlns="">...</Order>
</PurchaseOrderMessage>
</s:Body>
</s:Envelope>
|
So … having the contract declaration in place, we can build the
service. With your knowledge from the previous
parts
of this series, the seller side is (almost) straightforward to implement. I create
a SellerService supporting the defined ISeller interface and
write all operations (methods) in a similar fashion. First I dump out the
content of the incoming message and an artificial instance identifier I use to
play with instancing. The only “magic” is in how I obtain the
callback channel that I need to be able to send my answers to the other side. To
be precise, the magic isn’t mine, it’s sitting inside Indigo. The
call IBuyer
buyer = OperationContext.Current.GetCallbackChannel<IBuyer>() yields a ready-to-use channel that is
properly configured and bound to the “other side”. Having that in
hands, I cook up an answer (or two, or none, as you can see below) and send
that to “the buyer”. The hosting class and the service host are
standard fare.
|
using System;
using System.Xml;
using System.ServiceModel;
using System.Runtime.Serialization;
namespace DuplexMessagingConversation
{
[ServiceBehavior(InstanceMode
= InstanceMode.PrivateSession)]
class SellerService : ISeller
{
Guid
instanceId = Guid.NewGuid();
public
void HandlePurchaseOrder(PurchaseProcessHeader process, XmlNode data)
{
Console.WriteLine("Seller: Purchase Order
Received\n\t{0}\n\tInstance {1}",
data.OuterXml, instanceId);
IBuyer buyer = OperationContext.Current.GetCallbackChannel<IBuyer>();
XmlDocument orderConfirmation = new XmlDocument();
orderConfirmation.LoadXml("<OrderConfirmation>...</OrderConfirmation>");
buyer.HandlePurchaseOrderConfirmation(process, orderConfirmation);
XmlDocument invoice = new
XmlDocument();
invoice.LoadXml("<Invoice>...</Invoice>");
buyer.HandleInvoice(process, invoice);
}
public
void HandlePaymentNotification(PurchaseProcessHeader process, XmlNode data)
{
Console.WriteLine("Seller: Payment
Notification Received\n\t{0}\n\tInstance {1}",
data.OuterXml, instanceId);
IBuyer buyer = OperationContext.Current.GetCallbackChannel<IBuyer>();
XmlDocument shippingNotification = new XmlDocument();
shippingNotification.LoadXml("<Shipped>...</Shipped>");
buyer.HandleShippingNotification(process, shippingNotification);
}
public
void HandleShippingConfirmation(PurchaseProcessHeader process, XmlNode data)
{
Console.WriteLine("Seller: Shipping
Confirmation Received\n\t{0}\n\tInstance {1}",
data.OuterXml, instanceId);
}
}
class Seller
{
ServiceHost<SellerService> serviceHost;
public
void Open()
{
serviceHost = new ServiceHost<SellerService>();
serviceHost.Open();
}
public
void Close()
{
serviceHost.Close();
}
}
}
|
The buyer-side’s service implementation looks almost identical. The
one significant difference here is that the buyer is (in the self-hosted
scenario I have here: must be) a singleton within the scope of the
conversation. That means that the initiator of the conversation (what we
usually call “client”) will have to create a service instance and
hand that down into the infrastructure. Because I want to know when the
conversation is over and can shut down my test program, I hand a ManualResetEvent
to the service instance and have it Set it to signaled whenever the buyer’s
last expected message in the purchasing process arrives (shipping notification).
Otherwise the service implementation doesn’t have any more surprises.
More interesting is the InitiatePurchase method. It predictably creates
a service host instance for the buyer service and a channel factory that we
need to send the first message (purchase order) to the seller. From there
onwards, things are a little different than in the previous examples.
As the next step, I create a “service site”, which acts as the manager
for the duplex conversation we’re setting up. The ServiceSite is
initialized with the service host and a newly created service instance. As I
indicated in the previous paragraph, that instance is a singleton for the
conversation; it’s not a singleton per-se.
Using the service site as an argument, I can now create a duplex channel
with a call to CreateDuplexChannel on the channel factory. The resulting
channel is set up to do everything necessary to listen for answers in the scope
of the conversation and to relay the required “send answers here”
info to the other side. If you look at the SOAP message above, you’ll see
how that back reference is flowing using a WS-Addressing wsa:From header, which
is a reasonable thing to do as per WS-Addressing
(see: 3. / [reply endpoint] paragraph).
Once I have the channel in hands, I create the custom header instance and a purchase
order document (well…) and send it off to the seller side. Once that’s
done, I hang out and wait until the conversation is over and subsequently shut
down.
|
Using System;
using System.Xml;
using System.ServiceModel;
using System.Threading;
namespace DuplexMessagingConversation
{
class BuyerService : IBuyer
{
Guid
instanceId = Guid.NewGuid();
ManualResetEvent
waitHandle;
public
BuyerService(ManualResetEvent waitHandle)
{
this.waitHandle = waitHandle;
}
public
void HandlePurchaseOrderConfirmation(PurchaseProcessHeader process, XmlNode data)
{
Console.WriteLine("Buyer: Purchase Order
Confirmation Received\n\t{0}\n\tInstance {1}",
data.OuterXml, instanceId);
return;
}
public
void HandleInvoice(PurchaseProcessHeader
process, XmlNode data)
{
Console.WriteLine("Buyer: Invoice
Received\n\t{0}\n\tInstance {1}",
data.OuterXml, instanceId);
ISeller seller = OperationContext.Current.GetCallbackChannel<ISeller>();
XmlDocument paymentNotification = new XmlDocument();
paymentNotification.LoadXml("<Payment>...</Payment>");
seller.HandlePaymentNotification(process, paymentNotification);
}
public
void HandleShippingNotification(PurchaseProcessHeader process, XmlNode data)
{
Console.WriteLine("Buyer: Shipping
Notification Received\n\t{0}\n\tInstance {1}",
data.OuterXml, instanceId);
ISeller seller = OperationContext.Current.GetCallbackChannel<ISeller>();
XmlDocument shippingConfirmation = new XmlDocument();
shippingConfirmation.LoadXml("<ShipmentReceived>...</ShipmentReceived>");
seller.HandleShippingConfirmation(process, shippingConfirmation);
waitHandle.Set();
}
}
class Buyer
{
public
void InitiatePurchase()
{
ServiceHost<BuyerService>
buyerHost = new ServiceHost<BuyerService>();
using (ChannelFactory<ISeller> channelFactory = new ChannelFactory<ISeller>("clientChannel"))
{
ManualResetEvent conversationDone = new ManualResetEvent(false);
using (ServiceSite
replyTarget = new ServiceSite(buyerHost,
new BuyerService(conversationDone)))
{
ISeller channel =
channelFactory.CreateDuplexChannel(replyTarget);
PurchaseProcessHeader header = new PurchaseProcessHeader();
header.OrderIdentifier = "1234567890";
XmlDocument purchaseOrderDocument = new XmlDocument();
purchaseOrderDocument.LoadXml("<Order>...</Order>");
channel.HandlePurchaseOrder(header, purchaseOrderDocument);
conversationDone.WaitOne();
replyTarget.Close();
}
channelFactory.Close();
}
buyerHost.Close();
}
}
}
|
The Program is simple and predictable; I am just posting it for
completeness and because I renamed the classes.
|
using System;
namespace DuplexMessagingConversation
{
class Program
{
static
void Main(string[]
args)
{
Seller server = new
Seller();
server.Open();
Buyer client = new Buyer();
client.InitiatePurchase();
Console.WriteLine("Press ENTER to quit");
Console.ReadLine();
server.Close();
}
}
}
|
The configuration file that goes with this example is of course a bit
different from the previous ones. The <client> section and the buyerClientBinding
binding configuration apply to the buyer side, and the <services> section
and the sellerBinding are for the seller side. These sections would be
respectively split across two configuration files, if we would host the sample
in two processes.
Of course, the buyer’s <client>/<endpoint> definition
for the channel refers to the buyerClientBinding. That binding defines
three required binding elements: <reliableSession> configures the
channel to use a reliable messaging session with default values, <compositeDuplex/>
enables duplex support and <tcpTransport/> selects the TCP
transport. The order of these elements is significant and defines how these “behaviors”
are stacked in the channel. Quite special is the clientBaseAddress
attribute of the <compositeDuplex/> element; this value is used as
the base URI to dynamically construct the endpoint on which replies shall be
received by the buyer instance for this conversation. The result of that
composition can be seen in the wsa:From element in the SOAP message above.
The seller-side configuration for the <service> and its <endpoint>
is largely equivalent to what I’ve explained in the previous examples. The
only real difference is that the sellerBinding binding now also defines
the required binding elements and behaviors I just pointed out.
|
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<system.serviceModel>
<bindings>
<customBinding>
<binding
configurationName="sellerBinding">
<reliableSession/>
<compositeDuplex/>
<tcpTransport/>
</binding>
<binding
configurationName="buyerClientBinding">
<reliableSession/>
<compositeDuplex clientBaseAddress="net.tcp://localhost/buyer/reply"/>
<tcpTransport/>
</binding>
</customBinding>
</bindings>
<client>
<endpoint address="net.tcp://localhost/seller"
bindingConfiguration="buyerClientBinding"
bindingType="customBinding"
configurationName="clientChannel"
contractType="DuplexMessagingConversation.ISeller,
DuplexMessagingConversation"/>
</client>
<services>
<service serviceType="DuplexMessagingConversation.SellerService,
DuplexMessagingConversation">
<endpoint
contractType="DuplexMessagingConversation.ISeller,
DuplexMessagingConversation"
address="net.tcp://localhost/seller"
bindingType="customBinding"
bindingConfiguration="sellerBinding" />
</service>
</services>
</system.serviceModel>
</configuration>
|
And, lastly, here’s the output:
|
Seller:
Purchase Order Received
<Order xmlns="">...</Order>
Instance eb628fce-ac56-43af-9326-5bfc62a101dc
Buyer:
Purchase Order Confirmation Received
<OrderConfirmation xmlns="">...</OrderConfirmation>
Instance c1ce0c0f-fb98-4432-86fb-c81ac7243295
Buyer:
Invoice Received
<Invoice xmlns="">...</Invoice>
Instance c1ce0c0f-fb98-4432-86fb-c81ac7243295
Seller:
Payment Notification Received
<Payment xmlns="">...</Payment>
Instance eb628fce-ac56-43af-9326-5bfc62a101dc
Buyer:
Shipping Notification Received
<Shipped xmlns="">...</Shipped>
Instance c1ce0c0f-fb98-4432-86fb-c81ac7243295
Seller:
Shipping Confirmation Received
<ShipmentReceived xmlns="">...</ShipmentReceived>
Instance eb628fce-ac56-43af-9326-5bfc62a101dc
Press
ENTER to quit
|
Again, the messages are free form XML, so I am using Indigo strictly as a raw
messaging platform. It’s just a bit more powerful. If I’d show
you a functionally equivalent application based on System.Messaging and
MSMQ, you wouldn’t be done reading, yet.
[You should read Part 1 of this little series before
you proceed reading this one.]
In this 2nd part I am extending the simple messaging example of
Part 1 by adding some explicit WS-Addressing trickery. Addressing is so
fundamental that its properties are baked right into the Headers collection
of the Indigo Message. Even though there are (and I will eventually show)
much easier ways to do request/reply management that hide most of what I am
doing here very conveniently under the covers, I’ll give you an example
of how you can send messages to a service and then instruct the service to
explicitly reply back to an endpoint you provide. To make it a little more fun,
I am setting up two alternate reply endpoints and have the service come back to
them in turns. The Program host class is identical to the one the
previous example, so I’ll show only client and service code along with
the config.
The server-side code below grew a little bit as you can see. Now, there is a
IGenericReplyChannel that is the contract for the replies. It looks suspiciously
like the client-side’s IGenericMessageChannel and it is indeed a
copy/paste clone of it. I just didn’t want to share code between client
and server side. The Receive method has changed insofar as that it no
longer prints the message to the console, but now creates a reply and sends the
reply to the endpoint that the client indicates through the (WS-Adressing-) ReplyTo
header of the incoming message.
To do this, the service constructs a ChannelFactory< IGenericReplyChannel>,
using the endpoint address indicated in the incoming message’s ReplyTo
header and getting the binding information from the “replyChannel”
client setting in the config file shown further down. (Note that this is a bit
simplistic, because it assumes that the ReplyTo EPR uses a compatible
binding. There is a brilliant way to fix this, but … later). Then, the
message body of the incoming message is read into an XmlDocument and if
this was a real application, it would likely do something here. For now, we
just leave the content as it is and punt it back out.
To construct the reply message, I don’t use the CreateReplyMessage()
method provided on the Message class, simply because it doesn’t
have an appropriate overload to deal with an XmlReader in the same way
as Message.CreateMessage() does. I am sure that’s a minor
oversight that’s just a problem with my particular Indigo build. Creating
a reply is quite simple, though. All I need to do is to copy the incoming
message’s MessageID value into the RelatesTo.Reply property
of the outgoing message. For simplicity, I don’t check whether that header
is present and set, which I really should do, because there is no actual contract
or policy in place (for now). Once I have the reply constructed, just copying
the incoming body into it, I send it out through a channel (“proxy”)
constructed by channel factory.
|
using System;
using System.Xml;
using System.ServiceModel;
using System.Runtime.Serialization;
namespace SimpleAddressing
{
[ServiceContract]
interface IGenericMessageEndpoint
{
[OperationContract(IsOneWay = true, Action = "*")]
void Receive(Message
msg);
}
[ServiceContract]
interface IGenericReplyChannel
{
[OperationContract(IsOneWay = true, Action = "*")]
void Send(Message
msg);
}
class GenericMessageEndpoint
: IGenericMessageEndpoint
{
public void
Receive(Message msg)
{
using (ChannelFactory<IGenericReplyChannel> channelFactory =
new ChannelFactory<IGenericReplyChannel>(msg.Headers.ReplyTo,
"replyChannel"))
{
XmlDocument doc = new XmlDocument();
doc.Load(msg.GetBodyReader());
// There is a
msg.CreateReplyMessage(...), but that is missing the XmlReader ctor overload
using (Message reply = Message.CreateMessage("urn:some-action-reply",
new XmlNodeReader(doc)))
{
reply.Headers.RelatesTo.Reply = msg.Headers.MessageID;
IGenericReplyChannel
replyChannel = channelFactory.CreateChannel();
replyChannel.Send(reply);
}
channelFactory.Close();
}
}
}
class Server
{
ServiceHost<GenericMessageEndpoint> serviceHost;
public void
Open()
{
serviceHost = new ServiceHost<GenericMessageEndpoint>();
serviceHost.Open();
}
public void
Close()
{
serviceHost.Close();
}
}
}
|
Having a reply-enabled server-side, we can now get to the juicy part: the
client. Since we now need to listen for replies, the client has to expose a
reply-endpoint and therefore also act as a server. (That is the reason why “endpoint”
is preferred in service-land rather than the “client”/”server”
nomenclature). Therefore, I define a IGenericReplyEndpoint contract (no
surprises there) and implement that in GenericReplyEndpoint. To make the
example a bit more fun, the constructor of that service class takes two
arguments: The client argument refers to an instance of the Client
application class and epName gives the service instance (!) a name. The client
reference is used to let the client application know how many messages were already
received so that it can shut down, once the expected replies for all sent
messages have come back. The notification about received messages is done inside
the ReceiveReply method, which otherwise just writes the message body
to the console.
Unlike the previous example, this service implementation isn’t used
directly. Instead, I derive two subclasses from it: ReplyEndpointA and ReplyEndpointB.
These two classes each implement a constructor that passes “A” and “B”,
respectively, for the epName argument to the base-class and pass-through
the client argument. In case you wonder how the ServiceHost could
possibly construct instances of these service classes, not knowing the
appropriate parameters to pass to them: Instances of these two classes are pre-constructed
and fed into the service host as singletons as you will see below.
|
using System;
using System.Xml;
using System.ServiceModel;
using System.Threading;
namespace SimpleAddressing
{
[ServiceContract]
interface IGenericMessageChannel
{
[OperationContract(IsOneWay = true, Action = "*")]
void Send(Message
msg);
}
[ServiceContract]
interface IGenericReplyEndpoint
{
[OperationContract(IsOneWay = true, Action = "*")]
void ReceiveReply(Message msg);
}
class GenericReplyEndpoint
: IGenericReplyEndpoint
{
Client client;
string epName;
public GenericReplyEndpoint(Client client, string
epName)
{
epName = epName;
client = client;
}
public void
ReceiveReply(Message msg)
{
XmlDictionaryReader xdr =
msg.GetBodyReader();
Console.Write("{0}: ",
epName);
Console.WriteLine(xdr.ReadOuterXml());
client.MessageReceived();
}
}
class ReplyEndpointA
: GenericReplyEndpoint
{
public ReplyEndpointA(Client client):base(client,
"A")
{
}
}
class ReplyEndpointB
: GenericReplyEndpoint
{
public ReplyEndpointB(Client client)
: base(client, "B")
{
}
}
… continued below …
|
The Client application class is a bit more intricate than the previous
version, but there is no rocket science there. I have a counter for the number of
messages received and a ManualResetEvent that is getting signaled whenever
the number of received messages matches (or exceeds) the number of sent
messages. That happens in the MessageReceived method, which is called by
the service singletons. The class also has a UniqueIDGenerator, which is
an Indigo-supplied class that lets me generate values for the MessageID
header that is required alongside using ReplyTo.
In the SendLoop method, I now create two service host instances that
shall receive the replies to messages I send; one of type ServiceHost<ReplyEndpointA>
and one of type ServiceHost<ReplyEndpointB>. Each of these hosts
receives an instance of its service type as a construction argument. Doing so
causes the service host to operate in singleton mode, meaning that it will not
create new service instances out of and by itself, but rather use only the
exact instance supplied here. In the actual send loop, I alternate (i%2==0)
between those two service hosts and invoke SendMessage passing the
channel factory (not the channel as in the previous example) and the chosen ServiceHost
instance.
In SendMessage, I do a few simple things and only one not-so-obvious
thing. A new message is constructed as the first step and loaded with an action
and the body content. Then I grab the destination address from the channel
factory, which sits in the channel factory’s Description.Endpoint.Address
property and assign that to the message’s To header. The MessageID
is set to a new unique identifier created using the messageIdGenerator.
All that is pretty straightforward. Not immediately clear might be what I am
doing with the ReplyTo header:
Once a service host is Open, it’s bound to set of endpoints and
is actively listening on those endpoints using “endpoint listeners”.
I am writing “set of endpoints”, because a service might have
several. Each service can expose as many endpoints as it likes; each with a
separate binding (transport/behavior/address) and each with a separate
contract. There are puzzling special cases, of which you’ll see at least
one in this series, where a service listens and properly responds to a contract
type that is nowhere to be seen on the actual service implementation. The active
endpoints sit on the EndpointListeners collection.
For simplicity (again, this is a bit naïve, but serves the purpose for the
time being) and to obtain a ReplyTo address to pass to the service I am sending
the message to, I reach into that collection and grab the first available
endpoint listener’s address. What I should be doing here is to check
whether that listener is indeed the one for the IGenericReplyEndpoint contract
and whether I can find one with a binding that is mostly compatible with the
one the outbound channel uses. The latter selection would be done to make sure
that if I send out via “net.tcp” and I expose a “net.tcp”
endpoint myself, I would preferably pass that endpoint instead of a possible “http”
endpoint I might be listening on at the same time. Once ReplyTo is set,
I send the message out.
|
…
continuation from above …
class Client
{
const int
numMessages = 15;
int messagesReceived;
ManualResetEvent allReceived;
UniqueIDGenerator
messageIdGenerator;
XmlDocument contentDocument;
public Client()
{
messagesReceived = 0;
allReceived = new ManualResetEvent(false);
messageIdGenerator = new UniqueIDGenerator();
contentDocument = new XmlDocument();
contentDocument.LoadXml("<rose>is
a</rose>");
}
void SendMessage(ChannelFactory<IGenericMessageChannel>
channelFactory,
ServiceHost
replyService)
{
XmlNodeReader content = new XmlNodeReader( contentDocument.DocumentElement);
using (Message
msg = Message.CreateMessage("urn:some-action",
content))
{
msg.Headers.To = channelFactory.Description.Endpoint.Address;
msg.Headers.MessageID = messageIdGenerator.Next();
msg.Headers.ReplyTo =
replyService.EndpointListeners[0].GetEndpointAddress();
IGenericMessageChannel
channel = channelFactory.CreateChannel();
channel.Send(msg);
}
}
public void
SendLoop()
{
ServiceHost<ReplyEndpointA> replyServiceA = new ServiceHost<ReplyEndpointA>(new
ReplyEndpointA(this));
replyServiceA.Open();
ServiceHost<ReplyEndpointB> replyServiceB = new ServiceHost<ReplyEndpointB>(new
ReplyEndpointB(this));
replyServiceB.Open();
using (ChannelFactory<IGenericMessageChannel> channelFactory =
new ChannelFactory<IGenericMessageChannel>("clientChannel"))
{
channelFactory.Open();
for (int
i = 0; i < numMessages; i++)
{
if (i % 2 == 0)
{
SendMessage(channelFactory, replyServiceB);
}
else
{
SendMessage(channelFactory, replyServiceA);
}
}
channelFactory.Close();
}
allReceived.WaitOne();
replyServiceA.Close();
replyServiceB.Close();
}
public void
MessageReceived()
{
if (++ messagesReceived >=
numMessages)
{
allReceived.Set();
}
}
}
}
|
What’s left is the matching configuration for this. The mechanics of
how the configuration maps to the classes and instances are largely the same as
in the simple messaging example. A small difference is that the replyChannel
client configuration has no target address attribute, because that one
is always supplied via ReplyTo (refer to the GenericMessageEndpoint’s
Receive method above to see how that is wired up). Oh, yes, and I
switched it all to http transport in case you don’t notice. TCP would
work just as well, but I felt like I needed a little change. The assumed
assembly name for this sample is “SimpleAddressing”, of course.
|
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<system.serviceModel>
<bindings>
<customBinding>
<binding configurationName="defaultBinding">
<httpTransport/>
</binding>
</customBinding>
</bindings>
<client>
<endpoint address="http://localhost/genericep"
bindingConfiguration="defaultBinding"
bindingType="customBinding"
configurationName="clientChannel"
contractType="SimpleAddressing.IGenericMessageChannel,
SimpleAddressing"/>
<endpoint
bindingConfiguration="defaultBinding"
bindingType="customBinding"
configurationName="replyChannel"
contractType="SimpleAddressing.IGenericReplyChannel,
SimpleAddressing"/>
</client>
<services>
<service serviceType="SimpleAddressing.GenericMessageEndpoint,
SimpleAddressing">
<endpoint contractType="SimpleAddressing.IGenericMessageEndpoint,
SimpleAddressing"
address="http://localhost/genericep"
bindingType="customBinding"
bindingConfiguration="defaultBinding" />
</service>
<service serviceType="SimpleAddressing.ReplyEndpointA,
SimpleAddressing">
<endpoint contractType="SimpleAddressing.IGenericReplyEndpoint,
SimpleAddressing"
address="http://localhost/genericreplyA"
bindingType="customBinding"
bindingConfiguration="defaultBinding" />
</service>
<service serviceType="SimpleAddressing.ReplyEndpointB,
SimpleAddressing">
<endpoint contractType="SimpleAddressing.IGenericReplyEndpoint,
SimpleAddressing"
address="http://localhost/genericreplyB"
bindingType="customBinding"
bindingConfiguration="defaultBinding" />
</service>
</services>
</system.serviceModel>
</configuration>
|
The output of the sample is predictable, isn’t it? The replies come
back in sequence, alternating between the two reply services “A”
and “B”.
|
B:
<rose>is a</rose>
A: <rose>is a</rose>
B: <rose>is a</rose>
A: <rose>is a</rose>
B: <rose>is a</rose>
A: <rose>is a</rose>
B: <rose>is a</rose>
A: <rose>is a</rose>
B: <rose>is a</rose>
A: <rose>is a</rose>
B: <rose>is a</rose>
A: <rose>is a</rose>
B: <rose>is a</rose>
A: <rose>is a</rose>
B: <rose>is a</rose>
Press ENTER to quit
|
This weekend I will create some samples for myself as a foundation for
learning and fiddling around with Indigo bindings. A “binding” is a
combination of transport and behavior settings that binds a service contract to
an endpoint and it is a conceptual and functional superset of what wsdl:binding
does. One of the great things about Indigo is that changing bindings and
therefore adding/removing capabilities and even adding/exchanging/removing
transports can be done with no impact on the code itself. All of that can be
done in configuration. So what I will do is to share the base samples with you
as I write them, ex |