Tuesday, April 28, 2009

Domain Driven RIA: Managing Deep Object Graphs in Silverlight-3

Using RIA Services, can a simple n-tier application manage a deep object graph with eager fetching, lazy loading and silverlight databinding?




Downloads

Note: If you have no experience with RIA Services then you may prefer to start with my previous demo, A Domain-Driven DB4O Silverlight-3 RIA, which has links to RIA Services documentation and Microsoft presentations to get you started.

Introduction
RIA Services is a Rich Internet Application (RIA) framework that promises to streamline n-tier Line of Business application development. Reading through the RIA documentation and listening to the RIA team's presentations I was struck by two things:
  • How potentially useful this framework was. 
  • How skewed the material was in favour of a data-driven design approach
In this post I want to investigate how RIA Services can be used in a Domain-Driven context with a special focus on how it can help with the eager and lazy loading of domain entities.

Where is the Database?
I have chosen not to use a relational database in this example. This is because I want to ensure that my domain instance data can be easily stored and retrieved in the most efficient and maintainable domain-centric manner. I have therefore elected to use an object datastore, in this case DB4O, which provides all the ease, speed and functionality I need.  For more information see:
The Technology Stack
  • Silverlight 3
    Handles the client-side application logic and user interface
  • RIA Services
    Provides the client<->server interaction and client-side domain
  • DB4O
    A server-side datastore for domain entity de/serialization
The Software
Here is a sneak preview of the software in action.



The Objectives
Using a combination of RIA Services and DB4O I want to test the following:
  • Server - When I fetch an instance of the aggregrate root class I expect its inner hierarchy be eagerly fetched.

  • Client - I want certain collections to be lazy-loaded and so remain unloaded until they are requested.

  • I do not expect to write my own WCF Service nor do I want to write and Data Transfer Objects (DTOs).

  • I want to databind my domain entities to silverlight controls. I expect the controls to correctly display my eagerly fetched data as well as handling lazy-loaded data.

  • Finally, I want to prove that new domain entities can be created on the client and efficiently serialized to the server-side data-store as a batched unit-of-work

The Domain


I have a small hierarchical domain consisting of a single User aggregate root that bounds a one-to-many inner collection of Holding Entities that each contain a further collection of Transaction entities. 

The test domain therefore consists of the following hierarchy:
  • User.Holdings[n].Transactions[n]
Here is the code for the domain hierarchy.

   3:  public abstract class Entity
   4:  {
   5:      [Key]
   6:      public Guid Id { get; set; }
   7:  }
   8:   
   9:  public partial class User : Entity, IAggregateRoot
  10:  {
  11:      public string Name { get; set; }
  12:      public string Password { get; set; }
  13:      private List<Holding> _holdings = new List<Holding>();
  14:      public List<Holding> Holdings
  15:      {
  16:          get { return this._holdings; }
  17:          set { this._holdings = value; }
  18:      }
  19:  }
  20:   
  21:  public partial class Holding : Entity
  22:  {
  23:      public Guid UserId { get; set; }
  24:      public string Symbol { get; set; }
  25:      private List<Transaction> _transactions = new List<Transaction>();
  26:      public List<Transaction> Transactions
  27:      {
  28:          get { return this._transactions; }
  29:          set { this._transactions = value; }
  30:      }
  31:  }
  32:  
  33:  public class Transaction : Entity
  34:  {
  35:      public Guid HoldingId { get; set; }
  36:      public TransactionType Type { get; set; }
  37:      public int Quantity { get; set; }
  38:      public decimal Price { get; set; }
  39:  }

Domain Loading Strategy
  • Server
    Fetching a User should eagerly fetch all of its dependent Holdings. Each Holding should eagerly fetch all its dependent Transactions.

  • Client
    Fetching a User should eagerly fetch all of its dependent Holdings. However due to the potential for large numbers of Transactions, each Holding should not fetch any Transactions instead the Transactions collection must be lazy-loaded.

The Datastore Setup
Before plunging in to the RIA Services code I want to show you just how easy it is to use the DB4O object database. 

In the web.config there are two application settings (shown below). 
  1. DataFile.Name
    Specifies the name of the DB4O datastore file held in the App_Data folder

  2. DataFile.GenerateSampleData
    Determines whether the datastore is reset with newly generated sample data whenever the Cassini web application is re-started (useful for testing). 
Important: Ensure the DataFile.GenerateSampleData setting is false if you want to retain any changes between application runs.

   1:  <appSettings>
   2:      <add key="DataFile.Name" value="DataStore.db4o"/>
   3:      <add key="DataFile.GenerateSampleData" value="true"/>
   4:  </appSettings>
   5:  
   6:  public static void ServerOpen()
   7:  {
   8:      if (db4oServer != null)
   9:      {
  10:          return;
  11:      }
  12:   
  13:      var filename = Path.Combine(HttpContext.Current.Server.MapPath("~/App_Data"), ConfigFileName);
  14:   
  15:      var generateSampleData = bool.Parse(GenerateSampleData);
  16:      if (generateSampleData && File.Exists(filename))
  17:      {
  18:          File.Delete(filename);
  19:      }
  20:      db4oServer = Db4oFactory.OpenServer(GetConfig(), filename, 0);
  21:      if (generateSampleData)
  22:      {
  23:          SampleData.Generate();
  24:      }
  25:  }

In order to create the server-side eager fetch strategy outlined above, the DB4O datastore requires some configuration. The following GetConfig() method shows the Domain being scanned for types that implement IAggregateRoot with DB4O instructed to automatically fetch, save and delete the inner dependecies for those types.

   1:  private static IConfiguration GetConfig()
   2:  {
   3:      var config = Db4oFactory.NewConfiguration();
   4:      config.UpdateDepth(2);
   5:      var types = Assembly.GetExecutingAssembly().GetTypes();
   6:      for (var i = 0; i < types.Length; i++)
   7:      {
   8:          var type = types[i];
   9:          if (type.GetInterface(typeof (IAggregateRoot).Name) == null)
  10:          {
  11:              continue;
  12:          }
  13:          var objectClass = config.ObjectClass(type);
  14:          objectClass.CascadeOnUpdate(true);
  15:          objectClass.CascadeOnActivate(true);
  16:          objectClass.CascadeOnDelete(true);
  17:          objectClass.Indexed(true);
  18:      }
  19:      return config;
  20:  }

RIA Services
N-Tier applications are defined by the machine boundary that exists between the client and the server. Getting to grips with RIA Services begins by understanding how it tries to help you write applications that span that machine boundary. 

As you write your server-side domain code RIA Services tries to discover the way you intend to use this domain on the client. As it does so it generates a client-side version of your domain that fulfils those intentions. This means that you do not need to write a client-side version of your domain in order to use its features on the client, nor do you need to write any explicit mechanism for transferring domain instance data across the machine boundary (no WCF, no DTOs).

RIA Services discovers your intentions via a combination of Convention and Metadata. For example, I intend to utilize my User class on the client and so I need to be able to fetch User instances from the data store. This implies that somewhere I must write a server-side service method to perform the User fetch.

RIA Services simply asks that I put that User fetch service method in a class that derives from the RIA DataService class and that I follow some simple naming rules for the method signature. For more information on these conventions see .NET RIA Services Overview for Mix 2009 Preview

If I follow the prescribed conventions then RIA will  be able to determine that I intend utilizing the User class on the client and so generate a client-side version of my User class. This generated version is not the same class as my 'real' server-side User class, it only has as much or as little functionality as I decide to share (see later) but it does allow the client-code to operate as if I had access to the User class so I can use it in my silverlight code.

This is what the conventional User fetch method looks like.

   1:  [EnableClientAccess]
   2:  public class DataStore : DomainService
   3:  {
   4:   
   5:      public IQueryable<User> GetUser(string name, string password)
   6:      {
   7:          using (var db = DataService.GetSession<User>())
   8:          {
   9:              return db.GetList(x => x.Name.Equals(name) && x.Password.Equals(password)).AsQueryable();
  10:          }
  11:      }
  12:      ... other code
  13:  }

The presence of this method stimulates RIA into generating a client-side version of my User class however it will only carry over simple properties such as the User.Name and User.Password. So what happens if I want to make client-side use of a more complex property such as the User.Holdings collection? 

This is a new intention so I must tell RIA about it. Only then can RIA generate the appropriate client-side code to fulfil the new intention.

This is achieved in two steps.
  1. The Holding class must define a UserId property. When a new Holding is instantiated this property must be set to the Id of its parent User

  2. The User.Holdings Collection must be decorated with the appropriate attributes.
To decorate a  server-side domain entity with attributes targeted to client-side behaviour seems impure but fortunately RIA provides a pattern that brushes it all under the carpet and allows you to retain your domain-driven dignity. 

First you must ensure the main User class is partial. This allows you to create a new partial User segment in a separate code file called User.meta.cs. You can then add the following code to that file. In this way you can keep all the RIA meta-data tucked away in their own partial file segments.

   1:  [MetadataType(typeof (UserMetadata))]
   2:  public partial class User
   3:  {
   4:      internal sealed class UserMetadata
   5:      {
   6:          [Include]
   7:          [Association("User_Holdings", "Id", "UserId")]
   8:          public List<Holding> Holdings { get; set; }
   9:      }
  10:  }

You will note there are two attributes being used here. What are they doing? 
  • [Association]
    This attribute is informing RIA that the Holdings collection can be reconstructed on the client by comparing the User.Id to the Holding.UserId. When these match the Holding belongs to the collection. 

  • [Include]
    This attribute is more mysterious. Perhaps, like me, you might assume  it  means "Include this property in the generated code". This is not correct. In fact it means "Automatically recreate this collection on the client", in other words the client-side collection will be eagerly fetched and made available without any further intervention on your part. This is the behaviour we want for the User.Holdings collection and gives us our first clue about how we might set up the lazy loading for the Holding.Transactions collection.

RIA allows us to define the shape of our hierarchy on the client using a combination of convention for the fetch method signatures and metadata using the [Include] + [Association] attributes. But a class must also define functionality or it is just a DTO. 

Can I pick and choose the functions I want to appear in the client-side versions of my domain entities?

Sharing Domain Functions
On the client I want to add a new Holding to my User.Holdings collection. Being a conscientious domain-driven coder I want to ensure that my code follows the Law of Demeter, which means I cannot reach into the Holdings collection directly like this:

User.Holdings.Add(...)

Instead I need to write a method to do this for me:

User.AddHolding(...)

This is easy to write for my server-side domain but if I intend the same features to be available on the client I must tell RIA services about those intentions and so allow it to generate the appropriate client-side code.
  1. Ensure the class with shared features is partial

  2. Put the shared code in a partial segment stored a code file called MyClass.shared.cs

  3. Decorate the shared methods with the [Shared] attribute

Here is the code for the shared AddHolding method held in the User.shared.cs file.

   1:  public partial class User
   2:  {
   3:      [Shared]
   4:      public Holding AddHolding(Holding holding)
   5:      {
   6:          this.Holdings.Add(holding);
   7:          return holding;
   8:      }
   9:  }

More Shared Code

When I create a new Holding I would prefer to use a factory method found in my DomainFactory class. This is a useful method so I want it to be available on the client as well as the server. As it happens the Factory class also contains a number of methods I would like to share, so instead of creating a partial class and sharing out individual methods as before I can just share the entire Factory class.

The following code is held in a file DomainFactory.shared.cs

   1:  [Shared]
   2:  public class DomainFactory
   3:  {
   4:   
   5:      [Shared]
   6:      public static User User(string name, string password)
   7:      {
   8:          return new User
   9:          {
  10:              Id = Guid.NewGuid(),
  11:              Name = name,
  12:              Password = password,
  13:          };
  14:      }
  15:   
  16:      [Shared]
  17:      public static Holding Holding(User user, string symbol)
  18:      {
  19:          return new Holding
  20:          {
  21:              Id = Guid.NewGuid(),
  22:              UserId = user.Id,
  23:              Symbol = symbol
  24:          };
  25:      }
  26:   
  27:      [Shared]
  28:      public static Transaction Transaction(Holding holding, TransactionType type, int quantity, decimal price)
  29:      {
  30:          return new Transaction
  31:          {
  32:              Id = Guid.NewGuid(),
  33:              HoldingId = holding.Id,
  34:              Type = type,
  35:              Quantity = quantity,
  36:              Price = price
  37:          };
  38:      }
  39:  }

Some Client-Side Code

Now we have informed RIA about our intentions it is time to see some client-side code that shows the resulting RIA generated client domain in use. This code is taken from the silverlight application that accompanies the web application.

First of all, here is the code that does some setup and then the initial fetch for the User.

   1:  public HomePage()
   2:  {
   3:      this.InitializeComponent();
   4:      this._dataStore.Submitted += this.DataStoreSubmitted;
   5:      this._dataStore.Loaded += this.DataStoreLoaded;
   6:      this._dataStore.LoadUser("biofractal", "x", null, "LoadUser");
   7:      this.Holdings.SelectionChanged += this.Holdings_SelectionChanged;
   8:  }  
  12:  private void DataStoreLoaded(object sender, LoadedDataEventArgs e)
  13:  {
  14:      var userState = e.UserState;
  15:      if(userState==null)
  16:      {
  17:          return;
  18:      }
  19:      switch (userState.ToString())
  20:      {
  21:          case "LoadUser":
  22:              var user = e.LoadedEntities.First() as User;
  23:              this.User.DataContext = user;
  24:              this.Holdings.ItemsSource = user.Holdings;
  25:              break;
  26:      }
  27:  }

The _dataStore variable references an instance of the DataStore class which is derived from the RIA client-side DomainContext class. This class is auto-generated by RIA Services. It is the primary RIA generated artefact.

The DataStore.LoadUser() calls the GetUser() service method on the server. This is an asynchronous service call so the return must be caught in the DataStore.Loaded() event handler. Here the silverlight controls can be data-bound to their data sources and, because the User.Holdings collection was decorated with the [Include] attribute, RIA will ensure that it is automatically fetched. Using the Holdings collection as a binding data source will therefore display the correct list of Holdings for the current User without requiring an explicit fetch.

Lazy Loading the Transactions
In contrast to the User.Holdings collection, the Holding.Transactions collection is not automatically loaded when the User is initially fetched. Instead the client-side domain behaviour requires that the Transactions collection is lazy loaded on-demand. How is this achieved using RIA Services?

As before, the metadata is used to inform RIA of our intentions. The [Association] attribute is again used to decorate the collection definition in a partial class segment held in distinct code file (Holding.meta.cs). However this time there is no [Include] attribute.

   1:  [MetadataType(typeof (HoldingMetadata))]
   2:  public partial class Holding
   3:  {
   4:      internal sealed class HoldingMetadata
   5:      {
   6:          #region Properties
   7:  
   8:          [Association("Holding_Transactions", "Id", "HoldingId")]
   9:          public List<Transaction> Transactions { get; set; }
  10:   
  11:          #endregion
  12:      }
  13:  }

As a result RIA Services will generate the appropriate client-side code for the manipulation of Transactions however as there is no [Include] attribute RIA will not automatically fetch the members of a Transactions collection when its parent Holding is instantiated.

To manually load a list of Transactions it is necessary to write a parameterized server-side service method to perform the datastore lookup.

   1:  [EnableClientAccess]
   2:  public class DataStore : DomainService
   3:  {
   4:      ... other code
   5:   
   6:      public IQueryable<Transaction> GetTransactionsForHolding(Guid holdingId)
   7:      {
   8:          using (var db = DataService.GetSession<Transaction>())
   9:          {
  10:              return db.GetList(x => x.HoldingId.Equals(holdingId)).AsQueryable();
  11:          }
  12:      }
  13:  }

The GetTransactionsForHolding(...) method is scanned by RIA Services causing it to generate a client-side equivalent method on the DataStore class. This can then be used in client-side code to fetch a set of Transactions belonging to a specified Holding. The code below shows this happening. The call is being made within the SelectionChanged event of the Accordion control.

   1:  private void Holdings_SelectionChanged(object sender, SelectionChangedEventArgs e)
   2:  {
   3:      if (e.AddedItems.Count == 0)
   4:      {
   5:          return;
   6:      }
   7:      var holding = e.AddedItems[0] as Holding;
   8:      if (holding == null || holding.Transactions.Count > 0)
   9:      {
  10:          return;
  11:      }
  12:      this._dataStore.LoadTransactionsForHolding(holding.Id);
  13:  }

When an Accordion item is opened by a user click it fires the SelectionChanged event above. The newly selected Holding is extracted from the Accordion and its Holding.Id is passed into the RIA generated LoadTransactionsForHolding(...) method. This automatically calls the GetTransactionsForHolding(...) service method which returns the appropriate list of Transactions for the specified Holding.Id.

Where do these Transactions go? How is it that simply calling this method automatically fills the correct Holding.Transactions collection and displays that collection in the data-bound Accordion?

The list of Transactions is loaded into a flat list of Transactions generated and maintained by RIA Services. When a Holding.Transactions collection is requested RIA will dynamically create and return the correct list of Transactions as a conseqence of the information specified in the [Association] attribute. This is why each Transaction needs a HoldingId and each Holding a UserId. Finally, because RIA generated collections are ObservableCollections then changes automatically stimulate any data-bound containers to refresh themselves.

This means that a call to the LoadTransactionsForHolding() method will set off a chain of events that results in the lazy-loading of the selected list of Holding.Transactions and its subsequent display in the newly expanded Accordion item.

Creating and Saving Domain Instances
RIA Services makes the creating and saving new domain instances particularly easy. Once again the process begins with a statement of intention. This time RIA must be informed of our intention to add new Holdings to the User.Holdings collection and new Transactions to the Holding.Transactions collection. This is achieved via convention, by adding service methods whose signatures follow the convention shown below. 


   1:  [EnableClientAccess]
   2:  public class DataStore : DomainService
   3:  {
   4:      ...other code
   5:   
   6:      public void CreateHolding(Holding holding)
   7:      {
   8:          using (var db = DataService.GetSession<User>())
   9:          {
  10:              var user = db.GetFirst(x => x.Id.Equals(holding.UserId));
  11:              user.AddHolding(holding);
  12:              db.Save(user);
  13:          }
  14:      }
  15:   
  16:      public void CreateTransaction(Transaction transaction)
  17:      {
  18:          using (var db = DataService.GetSession<Holding>())
  19:          {
  20:              var holding = db.GetFirst(x => x.Id.Equals(transaction.HoldingId));
  21:              holding.AddTransaction(transaction);
  22:              db.Save(holding);
  23:          }
  24:      }
  25:  }

Adding these service methods tells RIA that we intend to add new Holdings and Transactions via client-side code. Without these methods any attempt to add an item will result in a runtime error. For example, if the CreateHolding() method above is commented out and a new Holding is added to the User.Holdings collection via client-side code, the following error is displayed.




Serializing New Entities
Domain entities added on the client are not automatically serialized to the server-side data-store. Instead RIA services keeps track of the changes you have made so that when a save is requested only the changes are submitted for server-side serialization. 

This is a good example of the Unit of Work pattern and in this way RIA helps to minimise the traffic over the wire as well as giving you much more flexibility with respect to rolling back or cancelling changes, providing save on demand or automatic timed-interval saves.

The following code shows how to add and save new domain items.


   1:  public partial class HomePage : Page
   2:  {
   3:      private readonly DataStore _dataStore = new DataStore();
   4:      private ProgressDialog _progressDialog;
   5:   
   6:      public HomePage()
   7:      {
   8:          this.InitializeComponent();
   9:          this._dataStore.Submitted += this.DataStoreSubmitted;
  10:      
  11:          ...other code
  12:      }
  13:   
  14:      private void ShowProgressDialog(string message)
  15:      {
  16:          this._progressDialog = new ProgressDialog(message);
  17:          this._progressDialog.Show();
  18:      }
  19:   
  20:      private void DataStoreSubmitted(object sender, SubmittedChangesEventArgs e)
  21:      {            
  22:          if (e.EntitiesInError.Count() != 0)
  23:          {
  24:              this._progressDialog.ShowError();
  25:          }
  26:          else
  27:          {
  28:              this._progressDialog.Close();
  29:          }
  30:      }
  31:   
  32:      private void SubmitChanges_Click(object sender, RoutedEventArgs e)
  33:      {
  34:          this.ShowProgressDialog("Saving Changes...");
  35:          this._dataStore.SubmitChanges();
  36:      }
  37:   
  38:      private void NewHolding_Click(object sender, RoutedEventArgs e)
  39:      {
  40:          var user = ((User) this.User.DataContext);
  41:          if (user == null)
  42:          {
  43:              return;
  44:          }
  45:          user.Holdings.Add(DomainFactory.Holding(user, NewHoldingSymbol.Text));
  46:          this.Holdings.SelectedItem = this.Holdings.Items[this.Holdings.Items.Count-1];
  47:      }
  49:      private void Buy_Click(object sender, RoutedEventArgs e)
  50:      {
  51:          var holding = ((Button) e.OriginalSource).DataContext as Holding;
  52:          if (holding == null)
  53:          {
  54:              return;
  55:          }
  56:          holding.AddTransaction(DomainFactory.Transaction(holding, TransactionType.Buy, 42, 0.42m));
  57:      }
  58:   
  59:      private void Sell_Click(object sender, RoutedEventArgs e)
  60:      {
  61:          var holding = ((Button) e.OriginalSource).DataContext as Holding;
  62:          if (holding == null)
  63:          {
  64:              return;
  65:          }
  66:          holding.AddTransaction(DomainFactory.Transaction(holding, TransactionType.Sell, 42, 0.42m));
  67:      }
  68:   
  69:      ...other code
  70:   
  71:  }

This code shows how to add new domain items to their correct location in the domain hierarchy using the shared DomainFactory class discussed earlier. These changes are then asynchronously submitted as a batched unit of work to the server, displaying a progress dialog to keep the user informed. The return is trapped so that the progress dialog can be dismissed and any errors displayed.

The Verdict
How did RIA Services and DB4O manage?
  • Server - When I fetch an instance of the aggregrate root class I expect its inner hierarchy be eagerly fetched.
    The server-side domain de/serialisation behaviour was handled by DB4O. Being an object database it is simple to create this behaviour using a few lines of initialisation code.

  • Client - I want certain collections to be lazy-loaded and so remain unloaded until they are requested.
    RIA Services provides a set of attributes that allow both eager and lazy loading to be specified as client-side behaviour and wired up with minimal code.

  • I do not expect to write my own WCF Service nor do I want to write and Data Transfer Objects (DTOs).
    RIA Services replaces the explicit WCF layer with an implicit data transafer layer via its DomainService class and the data manipulation  methods you write to extend it. 

  • I want to databind my domain entities to silverlight controls. I expect the controls to correctly display my eagerly fetched data as well as handling lazy-loaded data.
    Because RIA Services generates its own observable collections the silverlight databinding flows smoothly with little intervention. The lazy loading of new data stimulates the silverlight bound controls to refresh and so display changes as they occur.

  • Finally, I want to prove that new domain entities can be created on the client and efficiently serialized to the server-side data-store as a batched unit-of-work.
    RIA Services implements a Unit of Work pattern that allows only those items that have been changed to be batched and serialized to the server-side data-store when required.

I think that RIA Services plus DB4O performed well in handling the demands of my simple Line of Business Rich Internet Application. I would certainly recommend you try it out for yourself to see what you think. Good Luck.

5 comments:

  1. Scott Lowe2:24 pm

    Thank you so much for publishing this information - I was beginning to think that it wasn't possible to have a rich hierachical domain model shared via RIA Services on the client side. I'm so relieved that it's possible!

    Thanks again,

    -- Scott

    Scott Lowe
    Manchester UK

    ReplyDelete
  2. I will echo what Scott says above.

    This post has been of great use to me, so thanks very much for posting it :)

    Regards,
    Steve
    Riga, Latvia

    ReplyDelete
  3. Thanks for posting this... very useful. Has Microsoft posted any similar screen casts relating to the more advanced Ria topics?

    I have not been able to find much which is strange for such a popular feature.

    Jean

    ReplyDelete
  4. Anonymous9:20 pm

    Great example of real scenarios with actual object graphs. Many of the out of the box samples have the simple "Person" example and no depth.

    ReplyDelete
  5. Anonymous1:41 pm

    Hi, I check your new stuff on a regular basis. Your writing style is witty, keep
    doing what you're doing!
    My blog - Blog Comment

    ReplyDelete