Formatters and Remoting
It's pretty obvious that Serialization Formatters are not only meant to aid in
object persistence, but also play a very important role in the marshaling of
objects across remoting boundaries.
Overall, when I look at the extensibility of the Remoting framework in .NET,
I'm throughly impressed. The MS people who worked on it did a very fine job in
making the framework as extensible as possible. However, there are some raw
spots, and how Serialization Formatters hook into the remoting infrastructure
is, imho, one of them.
The extensibility model of remoting is, at its very core, based on channels and
channel sink chains, developed on top of a set of interfaces like
IClientChannelSink and IServerChannelSink, and chains of sink providers that
act as class factories for the sinks themselves (based on IClientChannelSinkProvider
and IServerChannelSinkProvider
).
On the other hand, Serialization formatters are based on the implementation of
the IFormatter
interface; however, there's also the
not-so-well-known IRemotingFormatter
interface. The definition of
the two are very much alike, except IRemotingFormatter's
method
take an extra Header[]
argument. Both SoapFormatter and
BinaryFormatter implement the IRemotingFormatter
interface.
What's not so apparent is just how formatters plug into the remoting process.
If you look at the framework docs, you'll see that this is done a set of
classes implementing the IClientFormatterSink
interface (on the
client side), and the IChannelSinkBase/IServerChannelSink
interfaces
on the server side. IClientFormatterSink
is just an interface
combining the IChannelSinkBase
, IClientChannelSink
and
IMessageSink
interfaces.
As you can see, this just means that, in essence you have to implement two
channel sinks of your own to plug your custom formatter into the remoting
infrastructure, not to mention two extra sink provider classes! Where does that
leave the IFormatter
and IRemotingFormatter
interfaces?
Nowhere. Absolutely nowhere.
So here's where my rant is aimed: Why take the trouble to define the interfaces
if you're not going to use them anyway? Makes no sense to me. It just
seems like too much trouble writing full-fledged channel sinks just to do what
should be a much simpler task.
But that's not the worst part. The worst part is that formatter sinks are not really
full-fledged channel sinks. In reality, there are only two things in the IClientChannelSink
implementation of a formatter sink that are actually called by the runtime:
NextChannelSink, and AsyncProcessResponse(). The others, you can leave
unimplemented. Now, to me, somehow, this doesn't seem quite right. Thanks to
Ingo, I know understand why it was done this way. I was talking to him
earlier to day and I'm going to quote him on this, since he puts it much better
than I ever could:
If you question this design, you'd have to question the complete separation in
IMessageSink, IClientChannelSink, IServerChannelSink and maybe even
IDynamicSink - I tended to do this [question it] in the past as well but I
somehow changed my attitude towards it ;-)I now agree with MS' designers that it's a Good Thing to have two interfaces,
whereas one works on messages and the other works on stream/headers. So ...
when having two interface, you necessarily have one class which provides for
the transition between those two. Well, that's the IFormatterSink ;-).In front of the formatter, you've got IMessageSinks and after it you work with
IClientChannelSinks. However, to keep this consistent, the reply simply _has_
to be handled by IClientChannelSink for a formatter. Remember:
IClientChannelSink means dealing with stream/header wheras IMessageSink means
dealing with Messages ;-)
He's obviously got a point here. I can't, however, keep away the feeling that
something's smells fishy here... I still believe it could have been done
differently.
Ingo did point out to me something extremely important:
However, there should be an IMessageSinkProvider interface ...
this really bugs me. You have to create an IMessageSink via an
IClientChannelSinkProvider, thereby leading to a must-have implementation of
IClientChannelSink as well. This interface's implementation will quite likely
only throw exception on each method because it's programmed to deal with
messages instead of streams and thereby has to be placed in front of the
formatter.
I totally agree with this.
If you're still wondering why write channel sinks in the first place: Well, if
you look at the formatter sinks for the two default formatters in the framework
you'll quickly realize why: They need to manipulate transport headers!.
This, quite honestly, could have been achieved without the need for full
channel sink implementations. Instead, MS could have just added generic
formatter sinks that could be instructed to use a given IFormatter or
IRemotingFormatter implementation, and added a new interface to allow them to
manipulate the transport headers, if needed
. This would have taken a lot of trouble of writing remoting formatters away.
And no, IRemotingFormatter
is not the interface to use
here, for several reasons. For starters, IRemotingFormatter implementations
cannot change the headers passed in to them (well, at least they can't add new
ones). There's also a slight impedance mismatch between the Header[]
argument pass in to IRemotingFormatter
and the ITransportHeaders
interface used in channel sinks to deal with headers. So, what purpose does IRemotingFormatter
server in real life? As far as I can see, none at all. At least I couldn't spot
any place where its methods are actually called... Perhaps I missed something,
and if I did, I'd love to hear about it!
One of the downsides of all this is that it leads to a tight coupling between a
given formatter and the channel underneath, via the formatter sink. This can be
easily witnessed on the
Rotor sources where the default formatter sinks actually check what
channel they are working on top of, and adjust their behavior based on
it [1]. Or even worse, does things that should've been left to one of the
transport sinks underneath, like headers["__RequestVerb"] =
"POST";
[1] This is required, in part, by some of the protocols, like SOAP, which
require the transport headers manipulated. For example, the HTTP result code
might need to be changed if the server's sending a fault or an actual
respose...