The Data Repository

Context

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 the DevForce Application Framework

The DevForce Application Framework does not dictate a particular way of implementing a repository. The following is a suggested best-practice.

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. As is common with interfaces, the business logic will be developed against the interface rather than the actual implementation.

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.

public INotifyCompleted FindCustomersByNameAsync(string name, Action<IEnumerable<Customer>> onSuccess, Action<Exception> onFail)
INotifyCompleted INotifyCompleted is an Interface provided by DevForce to signal completion of an asynchronous operation. By returning INotifyCompleted, we enable these methods to be called from within a Coroutine.

Note: The DevForce Application Framework has the ability to wrap an INotifyCompleted as an IResult - the equivalent interface for Coroutines in Caliburn-Micro - using CoroutineFns.AsResult()
string name An optional list of parameters to qualify the query. In this example, the name by which we wish to search the customers.
Action<IEnumerable<Customer>> onSuccess A 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.

Note: By making onFail a required parameter, the developer at a minimum is forced to think about error handling.

The corresponding implementation could look something like this:

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

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

    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. The DevForce Application Framework provides a set of OnComplete() extension methods that simplify the waiting for the completion of the operation and the calling of the respective user callbacks. 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.

[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 create methods to create new objects in the data source, delete methods to delete objects and a save method to save pending changes.

[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 INotifyCompleted Save(Action onSuccess = null, Action<Exception> onFail = null)
    {
        EntitySaveOperation op = Manager.SaveChangesAsync();
        return op.OnComplete(onSuccess, onFail);
    }
}

Thanks to DevForce, we don’t need update methods. The DevForce Entities maintain a live connection to the EntityManager they come from, so any changes to those entities will be saved next time the Save method is called on the repository. One could argue by looking at the delete method above, that we don’t really need it, either. While the EntityAspect property is public, it should be considered a data access layer detail and therefore should not be accessed outside of the repository.

Last edited Oct 23, 2011 at 6:38 AM by marcelgood, version 18

Comments

No comments yet.