Search   

Sunday, June 25, 2017

ContainerBasedReplication

Register  Login

 Building a Website with Container Based Replication  
 


Download the solution from here.

This article describes the development of a system, consisting of a Website and a Desktop Application which both use the same set of persistent business classes. Thus they can share the same logic on the web server and on the desktop machines. While the Web Application provides an online catalog and the possibility of ordering products online, the Desktop Application can be used to send bills and process payment.

A System with 2 Databases

The two applications share a subset of the same data. But the Web Server needs to work with it's own database. Therefore we need a possibility to transfer objects from the Desktop Application to the Web Server and vice versa. NDO Enterprise Edition allows the replication of objects using distributable object containers.

Suppose, a user adds a new product into the catalog using the Desktop Application. The desktop application packs the new product object into a container and uses a Web Service offered by the Shop Website, to send the new product to the server. The server stores the product into it's own database. It is obvious, that the product needs the same object id (which means: the same primary key in the database table) on the server and on the client databases. Thus the orders placed on the Website relate to the same products stored in the Desktop Application database. This makes it possible to transfer the orders to the Desktop Application for further processing.

The Desktop Application is able to ask the Website for new orders using the Web Service and stores the orders in it's own database. After that the orders can be deleted on the Website. The advantage of this procedure is, that the server doesn't keep so much sensitive user data, minimizing the damage caused by a successful exploit.

Picture 1: The Website and the application use two different databases, but share parts of their data.

The Sample Application

Now let's have a look at the sample application, which can be downloaded here. The following lines describe the usage of the system.

Using the System

After opening the solution, make sure, that the Website project has a correct reference to the NDO.dll installed on your system (each NDO edition has it's own NDO.dll with different public key tokens). Open the property pages for the Website project, remove the reference to the NDO.dll and add it again using the Add Reference Button. (Note, that the other projects don't bother about the NDO.dll version because this project types allow setting the Specific Version property of the reference to False). The Solution should compile without errors after that.

As the next step, run the "Rebuild Databases" application. It will make sure, that the databases for the desktop application and the website have the right table structure and that the tables are empty.

Now start the web site. Right click at the "Default.aspx" page of the Website project and choose "View in Browser". The result will be as expected: You'll see the headline "Our catalog" but no catalog appearing. That's because the database is empty.

Let's enter some products. Start an instance of the Desktop Application. A window appears, with a TreeView showing the two nodes Customers and Products. As you might expect, these nodes don't have child nodes. Choose Master Data / New Product in the main menu. A dialog appears, in which you can enter a product. An example would be "Notebook, 1999" (or wouldn't you like to buy a new notebook?). After pressing OK the applications needs a second or two to display the notebook properties. That's because the product is not only stored in the Desktop Application's database, but also transferred to the Website using the Web Service. The first launch of a Web Service always needs it's time.

Now enter some more products. You'll see, that the Web Service call is remarkably faster than before.

After entering the products, it might be interesting to look at the contents of the two databases. The databases are mdb-Files residing in the solution directory. If you open the product table of the two databases using Access or the Visual Studio Server Explorer, you can see, that both tables contain the same products and that the product data rows have the same primary key value. That makes it possible to transfer orders relating to this products from the website to the Desktop Application.

If you now start the Website, a product catalog appears:

If you select a product with clicking at Into the Shopping Basket, the product will be shown in the shopping basket:

You can delete items in the shopping basket or add additional items. If you click at Checkout, you get a summary of the purchase and a form in which you can enter your personal data:

Clicking on Submit makes the purchase perfect. That's a neat little shop application. If you look into the Website database, you can find a Customer data row, an Order data row and your OrderDetail data rows.

Now close the Browser and start the Desktop Application again. Choose Action / New Orders Check from the main menu. After a second you can find a new Customer under the Customers node:

As you can see, the whole object tree of the order has been transferred to the Desktop Application. And if you take another look at the Website's database, you'll find, that all of the order data have been deleted - they are not needed anymore. On the other hand, if you look at the Desktop Application's database, you'll find the order there - that's why you can see it in the UI.

How the System is Built

Let's first have a brief look at the business logic in the BusinessClasses assembly. At this point of development you won't find any remarkable logic there. The classes encapsulate merely data fields and relations. In a real world system the classes would have more of what is called behavior. It is one of the benefits of object oriented programming, that we can reuse the behavior of class systems in different contexts, like the Website and the Desktop Application.

Note, that the classes are marked with the [Serializable] attribute. This is necessary to make the objects transportable using containers.

The Desktop Application consists of a TreeView and a PropertyGrid in which the properties of the object selected in the TreeView can be viewed. There is a base class TreeNodeBase. For each class in the system there exists a node class derived from TreeNodeBase, which helps showing the object and it's children in the TreeView.

Using the Facade and Gateway patterns

Probably the most interesting point of the system is the communication of the Desktop Application with the Web Service. There are several tasks the Web Service should cover:

  • Receiving new products
  • Deleting products
  • Checking for new orders
  • Deleting orders
  • Providing the current server time

In a growing system the Web Service will have to provide more functionality. So, if we write a web method for each and every of that functions we will get a ever changing Web Service interface. That's a bad thing. A better approach seems to be a web method, which can handle every request in a generic way. The following interface declaration shows the basic idea:

public interface IShopServiceGateway
{
    string Service(int serviceCode,
        string[] parameterStrings,
        string serializedObjectContainer);
}

We can tell the service, what to do, using the serviceCode parameter. We can provide parameters serialized as strings and a serialized object container, which can hold an object tree. The string return value may hold any serialized value or a serialized object container. This approach is called Gateway pattern.

The disadvantage of that approach is, that for every call to the service one has to encode the parameters. You'd probably like to avoid that encoding work in your calling context. Now, let's assume our service interface looks like that:

public interface IShopService
{
    void NewProduct(Product p);
    void DeleteOrders(DateTime dtUntil);
    DateTime GetCurrentTime();
    IList CheckForNewOrders(DateTime dtUntil);
    void DeleteProduct(Guid id);
}

That's what the server is implementing. But we can implement this interface on the client too. This implementation can do all the encoding work to translate the interface calls to gateway calls. This approach is called the Facade pattern. The client code now uses calls to the Facade. That way the code look as if the client code called the server directly. This approach improves the readability of the code.

The web service on the other hand translates calls to the gateway back to calls of the actual server implementation. The whole system is shown in the following picture:

The ShopService class is the actual Web Service class. The logic is implemented in the ShopServiceImpl class. The Web Service just translates the incoming calls and delegates it to calls to the ShopServiceImpl class. On the client side the Web Service Proxy is the class automatically generated by Visual Studio. By default this class gets also the name ShopService. Since it is a proxy for the server side ShopService class, it has the same interface, and even if it does not explicitly implement the IServiceGateway interface we can say, that it is nevertheless an implementation of that interface. The ShopServiceFacade class translates all client side calls into the respective gateway calls.

Working with ObjectContainers and ChangeSets

Implementing a service with the Facade/Gateway patterns is very easy, if we use NDO. The persistence manager provides a method GetObjectContainer. The container is essentially a serializable wrapper for an ArrayList. NDO puts an object (or a list of objects) as so-called root objects in the container and makes sure that the child objects of the root objects are loaded from the database. Using the SerializationFlags enumeration you can determine which child objects are loaded. Objects being in the Hollow state don't make it to the receiver of the container. You have also the possibility to provide an own implementation of the ISerializationIterator interface to determine, which objects remain hollow and which objects are loaded.

The containers can be serialized either to a string or to a stream using the two overloads of the Serialize method. The UseBinaryFormat flag of the SerializationFlags enumeration determines, whether the Soap- or the Binary Formatter should be used. Normally you'd use the Binary Formatter because it's less chatty and is able to serialize generic containers. Remember, that the persistent classes have to be marked with the Serializable attribute to be serializable via the NDO ObjectContainers.

Now have a look at the NewProduct method of the ShopServiceFacade class in the Desktop Application.

public void NewProduct(Product p)
{
    ClearParameters();
   
PersistenceManager pm = new PersistenceManager();
   
Product p2 = (Product) pm.FindObject(p.NDOObjectId);
   
ObjectContainer oc = pm.GetObjectContainer(p2,
        SerializationFlags
.MarkAsTransient | 
       
SerializationFlags.SerializeCompositeRelations);
    shopService.Service((
int)ServiceCode
.NewProduct, parameters, oc.Serialize());
}

We just get an ObjectContainer for the p2 object and send it as a string to the Web Service--that's it. The object has to be marked as transient before we send it to the receiving context. The MarkAsTransient flag is responsible for that. Only if the object is transient, the receiving PersistenceManager can store the object again in the Web Services database.

Note, that we look-up the object p2 in a new instance of the PersistenceManager. Why don't we send directly the p object? That's because the p object should remain in the Persistent state. If we made it transient (as it were the case while serializing the container) we'd get all kinds of trouble using the transient object in the UI.

The NewProduct method is called by the menuNewProduct_Click method of the Form1 class. That's the place, where the object is constructed, edited in the UI and stored in the Destop Application's database. Note, that in a real world application you'd probably move that code at another place, so that it is not part of the UI code. Note also, that there should be some code handling the situation, if an object can be stored locally but fails to be stored at the Web Service.

private void menuNewProduct_Click(object sender, System.EventArgs e)
{
   
Product p = new Product();
   
NewProductDialog dlg = new NewProductDialog(p);
   
if (dlg.ShowDialog() == DialogResult.OK)
    {
       
ProductNode pn = new ProductNode(p);
        productsNode.Nodes.Add(pn);
        treeView.SelectedNode = pn;
        pm.MakePersistent(p);
        pm.Save();
       
IShopService shopService = new ShopServiceFacade
();
        shopService.NewProduct(p);
    }
}

Now let's move over to the Web Service. Have a look at the Service method of the ShopService.asmx code file. If there is a valid serialized ObjectContainer, it will be deserialized:

ObjectContainer oc = new ObjectContainer();
if (serializedObjectContainer != ""
)
    oc.Deserialize(serializedObjectContainer);

If there are some parameter strings, they will be converted into objects using the NDOProperty class. After that, the ServiceCode of the incoming call is interpreted and the corresponding IShopService method is called. In case of the NewProduct ServiceCode the NewProduct method is called:

case ServiceCode.NewProduct:
    shopServiceImpl.NewProduct((
Product) oc.RootObjects[0]);
   
break
;

ShopServiceImpl is the actual implementation of the Web Service. Look at the NewProduct method:

public void NewProduct(Product p)
{
    PersistenceManager pm = PmFactory.PersistenceManager;
    pm.MakePersistent(p);
    pm.Save();
}

Normally the PersistenceManager would provide a new ObjectId for any object to be made persistent. But if the object has a valid ObjectId and the key type is GuidKey (which is the case in our sample) the PersistenceManager assumes that it should store the object with the given id. That's the essential point, why this scenario works.

If the Desktop Application asks for new orders we'll move an object tree from the Web Server to the Desktop Application. That works exactly the same way as we have seen with the Product objects. But there are several objects to be transported: A Customer, an Address, an Order, and one or more OrderDetail objects. All those objects have to be stored at the Desktop Applications database. But there are also objects which are not to be stored. The OrderDetail class has a relation to the product class. Since the product objects have yet been stored at the Desktop Application they should remain hollow before transporting the object tree using the container. That's what the flag SerializationFlags.SerializeCompositeRelations is for.

All objects in the tree reachable with composite relations cannot exist at the server and thus should be transported and stored in the Desktop Application's database. All objects in the tree reachable with associations may be master data still available at the Desktop Application. In our sample, that's the case. If you write an application, in which newly created objects are reachable via associations and should be transported to the Desktop Application and stored there, you should write a specialized implementation of the ISerializationIterator interface to make sure that these objects are transported and stored.

How to Use DataBinding in Web Applications

We don't discuss here, wheter DataBinding makes sense or not. You can use DataBinding with persistent objects, if you want and if it makes sense in your project. And .NET 2.0 has a comfortable support for DataBinding to objects.

If you place a GridView on a WebForm, a little Button with an arrow appears in the upper right corner of the GridView. If you click at this Button, you get a context menu, where you can choose among several options what you want to do with the grid. Choose Configure DataSource and select <New data source...> like shown in this picture:

In the next dialog, choose Object. You can specify a name for the DataSource in the same dialog.

In the next dialog you must use a Business Object. That's a type providing a function, which delivers a list of an object of the given type.

You have to write a class, which deliver that functionality. For getting product objects for our catalog, such a class might look like that:

public class ProductListProvider
{
    public ProductListProvider()
    {
    }
    public List<Product> GetProductList()
    {
        NDOQuery<Product> q = new NDOQuery<Product>(PmFactory.PersistenceManager);
        return q.Execute();
    }
}

In our sample, we want to show all products, thus that class is perfect for our purposes. But we could also write a dummy class with an empty method just for the Visual Studio Wizards. The OrderDetailListProvider is such a dummy class:

public class OrderDetailListProvider
{
    public List<OrderDetail> GetItemList()
    {
        return null;
    }
}

Enter the method, which delivers the typed list in the next dialog:

Now, as Visual Studio knows, which element types will be shown in the GridView, you can use the GridView properties to edit your columns:

If you click at the edit button ('...') you get a dialog like that:

Now you can choose, which columns you want to show. In the sample we chose to show the Name and Price properties of the product class. As third column we added an unnamed BoundField. Don't enter a DataField name there. This third column contains the link to the shopping basket.

To fill that third column, we generated a handler for the RowDataBound event of the GridView. You can find it in the method GridView1_RowDataBound in the Default.aspx code:

protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
  if (e.Row.RowType == DataControlRowType.DataRow)
  {
    SessionData sessionData = new SessionData();
    Product p = (Product)e.Row.DataItem;
    string s = @"<a href=""ShoppingBasketPage.aspx?ProductId=" + p.NDOObjectId.Id.Value;
    if (sessionData.SessionString != string.Empty)
    s += "&" + sessionData.SessionString;
    s += @""">Into the Shopping Basket</a>";
    e.Row.Cells[2].Text = s;
  }
}

The code simply generates a link to the ShoppingBasketPage.aspx with a ProductId parameter. The parameter is the id of the product appearing in the same row as the link. If the user clicks at that link, the ShoppingBasketPage.aspx knows, which product has been selected.

Note, that you can use the DataBinding in similar way for Windows Forms applications.

Conclusion

Using the same set of persistent business classes in the Desktop Application and the Website delivers much advantage. Both systems can reuse the same behavior. You can simply navigate from parent to child objects. There is a lot of code spared because no Data Access Layer is needed. The use of DataBinding is as easy as it is with DataSets. The application delivers a lot of functionality with very few code.

   Print