Up Using Cocktail

Data repositories

Last modified on April 12, 2012 22:21

In many applications, the business logic accesses data from data stores such as databases or Web services. Directly accessing the data can result in the following:

  • Duplicated code
  • A higher potential for programming errors
  • Weak typing of the business data
  • Difficulty in centralizing data-related policies such as caching
  • An inability to easily test the business logic in isolation from external dependencies



Objectives

Use the Repository pattern to achieve one or more of the following objectives:

  • You want to maximize the amount of code that can be tested with automation and to isolate the data layer to support unit testing.
  • You access the data source from many locations and want to apply centrally managed, consistent access rules and logic.
  • You want to implement and centralize a caching strategy for the data source.
  • You want to improve the code's maintainability and readability by separating business logic from data or service access logic.
  • You want to use business entities that are strongly typed so that you can identify problems at compile time instead of at run time.
  • You want to associate a behavior with the related data. For example, you want to calculate fields or enforce complex relationships or business rules between the data elements within an entity.
  • You want to apply a domain model to simplify complex business logic.

Solution

A repository separates the logic that retrieves the data from the business logic that acts on it. It mediates between the data source layer, in our case DevForce, and the business layers of the application. The separation between the data and business tiers has three benefits:

  • It centralizes the data logic.
  • It provides a substitution point for the unit tests.
  • It provides a flexible architecture that can be adapted as the overall design of the application evolves.

How to implement a Repository with Cocktail

Cocktail does not dictate a particular way of implementing a repository. The following is a simple approach that works for smaller applications. For a more scalable approach, see the unit of work approach demonstrated in the TempHire reference application.

Start with an Interface

Above we made the argument that the repository provides a substitution point for the unit tests and in general separates the business logic from the data access logic. To allow for substitution, each repository should have an interface, so that the actual implementation can be swapped out for different purposes.

Method signatures

To improve maintainability, readability and consistency of the code, we want to standardize method naming and method signatures. In particular this will ensure a consistent approach when dealing with asynchronous data sources as is the case in Silverlight.

A typical enterprise application performs disproportionally more read operations than it does write operations. It is very common that data gets accessed and presented in many different ways. Users can browse or search in order to find the data they are looking for, or dashboard type screens aggregate different sets of data to present them in a way that facilities fast decision making. For these reasons, a typical repository has many methods to retrieve data in the various forms needed by the business logic.

So, let’s focus on those read methods first. Typically we want to name the methods, so that the name conveys the purpose. We choose names that start with one of the following prefixes: Get, Fetch, Retrieve, Find, Select such as GetCustomer(), FindCustomers() or FetchCustomers(). The “Find” prefix is typically used for methods that involve a search, for example FindCustomersByName(). “Get” is typically used to retrieve a specific instance of an entity or a specific dataset, for example GetLastMonthsOrders().

Now that we have the naming convention squared away, let’s look at an example and discuss the signature. We’ll start with an example for an asynchronous data source.

C#
public OperationResult<IEnumerable<Customer>> FindCustomersByNameAsync(string name,
    Action<IEnumerable<Customer>> onSuccess = null, Action<Exception> onFail = null)
OperationResultOperationResult encapsulates the DevForce asynchronous operation.
string nameAn optional list of parameters to qualify the query. In this example, the name by which we wish to search the customers.
Action<...> onSuccessA user callback for the repository to invoke if the operation completes successfully and for the caller to receive the result of the operation. In this example, the result is some collection of customers matching the name.
Action<Exception> onFail  A user callback for the repository to invoke if an error occurs. The caller can decide if the error should be handled at this level by providing a delegate or at a higher level by providing null.

The corresponding implementation could look something like this:

C#
[Export(typeof(ICustomerRepository))]
public class CustomerRepository : ICustomerRepository
{
    [ImportingConstructor]
   public CustomerRepository(IEntityManagerProvider<NorthwindIBEntities> entityManagerProvider)
    {
        EntityManagerProvider = entityManagerProvider;
    }

   public OperationResult<IEnumerable<Customer>> FindCustomersByNameAsync(string name,
       Action<IEnumerable<Customer>> onSuccess = null, Action<Exception> onFail = null)
    {
        var q = Manager.Customers.Where(c => c.CompanyName.StartsWith(name));
        var op = q.ExecuteAsync();
       return op.OnComplete(onSuccess, onFail).AsOperationResult();
    }

   private IEntityManagerProvider<NorthwindIBEntities> EntityManagerProvider { get; set; }
   
   private NorthwindIBEntities Manager
    {
       get { return EntityManagerProvider.Manager; }
    }
}

As we can see, this implementation hides all the data access details. It gets the EntityManagerProvider constructor-injected by MEF in order to obtain an EntityManager and then composes and executes a corresponding query and passes the result after the query executes successfully to the user callback. Cocktail provides a set of OnComplete() extension methods that simplify the processing of the operation result. Notice the MEF Export attribute, which registers the repository by its interface in the MEF container, so that we can later constructor-inject the repository to any class that needs data.

The synchronous equivalent is as expected a bit simpler and doesn’t need much explanation. Gone are the callbacks.

C#
[Export(typeof(ICustomerRepository))]
public class CustomerRepository : ICustomerRepository
{
   // .... Details omitted for clarity ....

   public IEnumerable<Customer> FindCustomersByName(string name)
    {
        var q = Manager.Customers.Where(c => c.CompanyName.StartsWith(name));
       return q.ToList();
    }
}


Write Methods

Just like the read methods, the repository typically has methods to modify data.

C#
[Export(typeof(ICustomerRepository))]
public class CustomerRepository : ICustomerRepository
{
   // .... Details omitted for clarity ....

   public Customer CreateCustomer()
    {
        var c = Manager.CreateEntity<Customer>();
        Manager.AddEntity(c);
       return c;
    }

   public void DeleteCustomer(Customer customer)
    {
        customer.EntityAspect.Delete();
    }

   public OperationResult SaveAsync(Action onSuccess = null, Action<Exception> onFail = null)
    {
        EntitySaveOperation op = Manager.SaveChangesAsync();
       return op.OnComplete(onSuccess, onFail).AsOperationResult();
    }
}

Thanks to DevForce, we don’t need update methods. DevForce entities maintain a live connection to the EntityManager they came from, so any changes to those entities will be saved next time the SaveAsync method is called.

Tags: Cocktail
Created by DevForce on January 13, 2012 17:32

This wiki is licensed under a Creative Commons 2.0 license. XWiki Enterprise 3.2 - Documentation. Copyright © 2012 IdeaBlade