June 19th, 2006
I’m currently working on a client-server application with a .NET 2.0 WinForms GUI. We’re using the standard WebMethod RPC stuff to bounce things back and forward between the server and client, with .NET taking care of serializing everything into XML and back again and NHibernate handling persistence. So in theory, the only work should be designing our domain and our forms, binding the domain objects to the forms, defining some services, writing some Hibernate mapping files, and the occasional bit of business logic when things get dull.
Sadly it’s not to be. Coming from a LAMP / J2EE background, I’ve always been sceptical of the Evil Empire. However in the back of my mind I assumed that there must be something in it what with all the RAD integrated environment drag’n'drop hoopla with which sneering Microsoft weenies assault us poor UNIX kids. One early warning sign came when dealing with distributed transactions. Deep in the OS lurks the MSDTC stuff, which can be cajoled into action with the simple application of the TransactionOption propery in the WebMethod attribute. When things inevitably go wrong, I poke around the filing system in search of a log file. Sure enough we find one… in a binary format. There’s a batch file that promises to convert it into plain text, but luckily it depends on an executable which is only included in the Windows Driver Development Kit. 123Mb of download and installation later, and I discover that the transaction rolled back at the request of the client. Thanks.
Anyway that’s not the real problem. That comes with the limitations imposed by the combination of WebMethod and serialization. When saving from the GUI, we’d like to send an object graph via a WebMethod to have it persisted (if you’re already clutching your head, you can skip to the end). The first problem is that the standard serialization is unable to handle bidirectional associations, such that (for example) a badger keeper has a set of badgers, and each badger has a reference to its badger keeper. When it tries to deserialize the object it dies, saying it has found a circular reference. Furthermore, the standard XML serialization works on properties rather than (private) fields, so you can’t use immutable objects. This can be fixed by adding the IXmlSerializable attribute and using XStream or something similar to do the serialization yourself. However, WebMethod then fails to handle deserializing objects into fields defined with abstract types, saying it can’t instantiate an abstract class.
Another obstacle crops up if you try to serialize any kind of graph except a tree. For example your badgers might eat crumpets, and hence when modelling them you would give them a set of crumpets. It’s convenient for me, as a badger keeper, to have a reference to the same set of crumpets without having to ask the badger for them. However, when two objects both have a reference to a third object in this way, serialization again puts a fly in my crumpets by producing two copies of the third object. One of these copies is referred to by the first object, the other by the second. When Hibernate tries to save the graph, it croaks complaining that it has two references to the same entity. If the third object is a new one (we’ve bought a new crumpet and want to add it to our crumpet store), and the second of the referring objects (the badger) has cascade set to none in its Hibernate mapping (so that I have responsibility for moving around crumpets rather than the badger), you’ll find that Hibernate is unable to persist the graph. It dies because it has a reference to a transient object which will not be saved – the same object that the first object refers to, but a new instance with a new reference created by deserialization.
The most recent problem we’ve encountered occurs due to the client server model we’ve employed, in which objects are created on the client side and then persisted. One such object has a timestamp field which is required to be accurate for auditing purposes. However because it’s created on the client side, users who have their clock set wrongly or who maliciously alter it will generate spurious timestamps. These fields should be initialized on the server side.
None of these issues are show-stoppers, but working around them requires careful thought and in some cases some vigorous refactoring. More than anything, it demonstrates that you can’t just stick some WebMethod attributes on your service methods and expect to have an instant remote service layer. Furthermore, not all these problems are just dumb technical goof-ups on the part of Microsoft – they’re genuine modelling problems that can’t be fixed with clever coding. In particular, trying to serialize an object graph doesn’t just impose the irritating constraint that it must be a directed tree. It also causes problems with locking, synchronization and the problem of how to apply a security policy that varies for different nodes in the graph.
These considerations suggest using a single web service which takes a set of commands, perhaps one for each distinct part of the graph we want to persist. The set could be executed in a single transaction. This frees us from some of these issues, at the cost of incurring further complexity in our services layer: logic that turns a graph into a set of commands and then interprets them on the server side. It does have the advantage of significantly simplifying the schema of our web service by making it generic and weakly typed. We could then handle issues such as preserving backwards compatibility more simply, since they would not involve changes to this schema. Because we’re only using a single service entry point, it becomes possible to easily factor out properties common to all requests, such as authentication. We also get the benefit that we only have to serialize the parts of the graph we actually want to save rather than the whole lot, although this is not a primary consideration just yet. Oh and another thing: we get REST buzzword bingo points.
We’ll be evaluating this approach and some others over the course of the next week, but if anybody who has got this far has any experience in this area, your advice would be much appreciated.