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:
Use the Repository pattern to achieve one or more of the following objectives:
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:
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.
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.
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) |
| OperationResult | OperationResult encapsulates the DevForce asynchronous operation. |
| 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<...> 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. |
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(); } } |
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.