Overview

In my previous post, “Friendly UI Development process“, I introduced a UI application architecture that allows for a more “friendly” development process. It’s not in any way a new or revolutionary idea. In fact, nothing that I write in this post is revolutionary (I hope to introduce new ideas in later posts). These are only common sense guidelines to an architecture that helps me take projects for remote development. But even without remote development being involved, its still a recommended architecture. It is “Friendly” because it allows each team to focus on its core knowledge without stepping on each other’s toes. The UI team deals with UI. The Server team deals with server. Most of the code belongs to only a single team so the knowledge does not need to be shared. The only pieces of code that have mutual ownership contains small and simple objects and an interface between the client application and the back end server. Also, in order to allow the UI developers to test the application comfortably and in various scenarios without having to spend a long time configuring pieces of code that are developed in technologies that UI Developers need not be fluent in, there is a second UI application that simulates the server behavior. The Simulator is by itself a UI application, written in the same UI technologies as the original application, so that the UI developers feel at home with it, which is good, because they own it.

Sample Contract

To demonstrate the concept, lets take an extremely light example, a Library book loan management application. This application helps a librarian to manage the book loans. You can view the users that are registered in the system, see their current summary (how many books they loaned, how many late returns they had, and how many books they are currently holding). You can tell the system that the user loans a book, and you can tell it that the user returned a book. Each of these transactions require the system approval.

Remember that the server team will write an object that would be run on the client machine which serves as an abstraction layer between the application and the back end server. The contract is the interface that defines this layer. So how do you go about writing the contract?

Data Model

We start by creating the empty solution (called Library) and adding a class library project (called Library.Contract). We add 3 folders to the new project, as explained in the previous post: Interface, Model and Wrappers:

Library Contract 1

The data model should be defined in a way that allows the data objects to pass through communication channels, so the objects need to be serializeable. In order to define that, we need to add a reference to the System.Runtime.Serialization assembly.

Serialization DLL

Now, before we start with the data objects, I want to make life a little easier by defining a base class to all data entities. They all have a UID and I don’t want to repeat it so here is the base of all data entities:

EntityBase class

All the data objects will be decorated by the [DataContract] attribute. This attribute tells the serialization engine that this object is to be used as a data entity and to be serialized. The (IsReference=true) parameter is used when serializing graphs of objects which may include circles, to avoid infinite loops. Now each property that should be serialized is decorated with the [DataMember] attribute. Some data entities may have calculated properties or any other read only properties and these can not be serialized, so the data member attribute tells the serialization engine that this property is a normal, simple, data carrier and that it should be serialized.

Next we define 3 other entities. The Book, the User and the Loan which connects a single user with a single book:

Book Entity

User Entity

Loan Entity

Each entity defines several simple properties. In addition, the loan references a single book (using both the BookId and the Book properties) and a single user (using both the UserId and the User properties). So you may either get the id of the referenced objects, or the objects themselves (thus creating an object graph). Bare in mind that these objects are used to pass data around, back and forth, using the contract methods. The properties do not always have to be all filled (otherwise, we may pass the entire object graph in the database each time we want to send a single object). The virtual properties “Book” and “User” are there in cases where we want to send a loan object with the entire information of the book that was loaned and the user that loaned it, but we may leave them null when that information is not relevant for the method that was called. We still have the referenced book id and the referenced user id to tell us that these are not really empty. In the same way, the properties that hold the list of loans for a single user or a single book can also be left empty in cases where they are used as returned data for methods where the list of loans is not relevant. Note that these objects are passed back and forth, so the client may also construct these entities and fill them fully or partially with information to pass to the server. A potential UpdateBook(Book b) method may pass all the book information which will then be updated server side. The book may potentially only hold the ISBN and title, or it may hold an entire loans list.

The API

Now that we have defined the “nouns”, we can go ahead and define the verbs, the actions that the API allows the client to ask the server. These are defined in a single C# interface as follows:

Library - API

This file will sit in the “Contract” folder.

ILibraryApi Interface

Basically, what this interface says is this. “Any object instance, that implements the following methods, and returns the expected results, may be used as a server provider for the application“.

Wrapper Entities

You can see that we are using wrapper objects for some of the methods. This overcomes a language barrier where methods may not return more than a single entity. The “GetUserSummary” method, for example, needs to return the User details, in addition to some calculated data that does not reside inside the user entity such as the number of loans the user has performed, the number of non returned loans, and the number of loans which were returned late. Of course, we could pack the user’s entire list of loans inside the returned user entity, but if the application only wants to present their number then we prefer to save bandwidth and only pass what the application wants to display. To do that, we define a wrapper object that serves this method. Here is the definition of the UserSummaryResult wrapper:

UserSummaryResult Wrapper

In the same way, we define a wrapper object to be used to wrap together all the information returned after a request to either loan or return a book.

BookReturnResult Wrapper

Hidden Assumptions

You may have noticed that not all the terms of the contract between the client and the server have been stated. We have a few hidden assumptions. The API Method “GetCurrentLoans” takes a user id and returns a list of loan objects. But the contract does not state in any way if the application expects the returned loan objects to contain information about the book that was loaned in each one. That depends on the definitions of what the application presents when it uses this method. Unfortunately, the interface language does not allow us to define which properties should be filled and to which depth. So this is a constraint that can not be maintained by the compiler and therefore it must be maintained by us human programmers. Here, we have to remember that the contract package is a common package, it is used by both the server and the client teams, as means to define a way to work together. It is not only a piece of code to compile and run, it is also a documentation of the mutual expectations. Therefore, it is highly recommended, that you make sure all assumptions are stated. There is no bigger enemy to the friendly development process then a hidden assumption. If you can use code to state the assumption, great. If you can not, at least document the assumption in the most proper place. So after adding the hidden assumptions as comments, this is what the contract and wrappers should look like:

UserSummaryResult Wrapper

BookReturnResult Wrapper

ILibraryApi Interface

Note that I intentionally avoided adding description of “EVERYTHING” that each property or method should do. Documentation that is too detailed is often not read, and it is more difficult to maintain. Good documentation is one that:

1. Only states the non trivial information.

2. Is located where it is most probably going to be relevant.

If you add a comment to the “GetUserSummary” method that says that “this method fetches the user summary data”, then you did not add any information that the user does not already know just by looking at the method name. In fact, after encountering too many such cases, most users will stop reading the comments. When the user sees the “green” color above the code itself, it should tell him that there is some extra information thta it is important for him to know at this context, and we better not disappoint him with trivialities.

The details of the user summary wrapper are better located on the object itself, because the content of the object already provide 90% of this information in code, using meaningful property names and types. So you only add the “hidden” information in the context of the property that it is hidden under.

Summary

In this post we have discussed the way to create the Contract project that serves as an abstraction layer between the client and the server. We have talked about how to define the data entities so that they may be used as part of an object graph and be serialized that way. We have demonstrated how to create an interface that defines the collection of methods that the client may call on the server. We have demonstrated the use of Wrapper entities to pack data together. Finally we have talked about the importance of avoiding hidden assumptions as much as possible, either using code language features, or by using the good old comments. A clean, short, well placed, and non-trivial comment goes a long way to clear things up where it is mostly needed.