Need Quality Code? Get Silver Backed

Repositories in Practice

2ndMay

0

by Gary H

In the last article we talked about the repository pattern and how we can use it to make our code more testable. In this article we will look at an actual implementation of a repository pattern isolating our business logic from data access and coding against interfaces so that we can easily test the code we create. This article is targetted at accessing a database. The same principles can be used elsewhere but be aware of this bias.

Solution Setup

As a minimum we will need three class library projects. One for the DAL itself - this will consist of the data access code and concrete classes for modelling the data. The second will be for Interfaces - this will model the contracts that the DAL promises to adhere to and the data that will be modelled. The third will be for Inversion of Control (IoC) - a bridging project which should be the only project which references the DAL.

The separation of DAL and Interfaces is so that our business logic can take a dependency on the Interfaces only and as such we can be certain that we will not be able to accidentally reference concrete DAL classes in code. The separate IoC project is to enforce this and to act as a bridge, only the IoC project should reference the DAL, all other projects that need data access should reference the IoC project and use dependency injection to create repository instances.

Repository Interface

We start by determining the base functionality that all Repositories will utilise and then modeling this in an interface. In our repository we will expose Get, All, Delete and Save methods. We want our base repository to be able to handle as much of this grunt work as possible so that modelling simple data scenarios is as easy as we can make it. As such we will add a constraint to our base repository interface and restrict it to allowing only types that help us out by specifying the name of the table that they reside within and the name of the primary key.

public interface ITableKeyInformation
{
	string TableName { get; }

	string IdField { get; }
}

public interface IBaseRepository<T> where T : ITableKeyInformation
{
	T Get<TId>(TId id, IDbConnection existingConnection = null);

	IEnumerable<T> All(IDbConnection existingConnection = null);

	int Delete<TId>(TId id, IDbConnection existingConnection = null);

    int Save(T source, IDbConnection existingConnection = null);
}

Base Repository

Next up is to cut a base repository adhering to this contract. What we would really like is a base repository that works internally on concrete classes from our data model but which returns interfaces onto those models decoupling our data access from the implementation. We do this through some trickery on the constraints of our base repository:

public abstract class BaseRepository<TInterface,TConcreteClass> 
 : IBaseRepository<TInterface> 
	where TConcreteClass : class, ITableKeyInformation, TInterface, new() 
    where TInterface : ITableKeyInformation, IModelBase

The big where clauses here are in two parts, first:

where TConcreteClass : class, ITableKeyInformation, TInterface, new()

This says that the concrete class must be a class, must implement the ITableKeyInformation interface, must have a default constructor and must be convertible to the TInterface type. Second:

new() where TInterface : ITableKeyInformation

This says that the TInterface type must implement ITableKeyInformation. These constraints logically leave us with a repo that can use the concrete class for mapping our data access and for retrieving the tables key information but returns objects cast to the Interface implementation for contract based development and to make any form of IoC easier.

With the signature sorted we can go on to implement the base methods. We mark the methods themselves as virtual so that we can override the default implementation and mark the whole base repository class as abstract to prevent instantiation. This base repository sample assumes that all tables have a numeric ID field and is implemented in Dapper. You can add Dapper to your project using NuGet.

public abstract class BaseRepository<TInterface,TConcreteClass> 
 : IBaseRepository<TInterface> 
	where TConcreteClass : class, ITableKeyInformation, TInterface, new() 
    where TInterface : ITableKeyInformation, IModelBase
{
	private string _tableName;
	private string _idField;

	protected string TableName
	{
		get { return _tableName ?? (_tableName = 
							new TConcreteClass().TableName); }
	}
	
	protected string IdField
	{
		get { return _idField ?? (_idField = 
							new TConcreteClass().IdField); }
	}

	public virtual TInterface Get<TId>(TId id, 
						IDbConnection existingConnection = null)
	{
		var conn = existingConnection ?? CreateConnection();
		try
		{
			return conn.Query<TConcreteClass>(
				String.Format("SELECT * FROM {0} WHERE {1}=@ID", 
					TableName, IdField), 
					new {ID = id}).FirstOrDefault();
		}
		finally
		{
			if (existingConnection == null)
			{
				conn.Close();
				conn.Dispose();
			}
		}
	}

	public virtual int Delete<TId>(TId id, 
					IDbConnection existingConnection = null)
	{
		var conn = existingConnection ?? CreateConnection();
		try
		{
			return conn.Execute(
				String.Format("DELETE FROM {0} WHERE {1}=@ID", 
					TableName, IdField), 
					new { ID = id });
		}
		finally
		{
			if (existingConnection == null)
			{
				conn.Close();
				conn.Dispose();
			}
		}
	}

	public virtual IEnumerable<TInterface> All(IDbConnection 
											existingConnection = null)
	{
		var conn = existingConnection ?? CreateConnection();
		try
		{
			return conn.Query<TConcreteClass>(
				String.Format("SELECT * FROM {0}", TableName));
		}
		finally
		{
			if (existingConnection == null)
			{
				conn.Close();
				conn.Dispose();
			}
		}
	}

    public virtual int Save(TInterface source, 
    					IDbConnection existingConnection = null)
    {
        throw new NotImplementedException();
    }

	protected IDbConnection CreateConnection()
	{
        var c = new SqlConnection(
        	ConfigurationManager
        		.ConnectionStrings["DefaultDbConnName"]
        		.ConnectionString);
		c.Open();
		return c;
	}
}

Concrete Implementations

We can now start creating a concrete implementation for our repository. The concrete implementation requires three things: a concrete data model class, an interface to that data model and a concrete implementaion of the base repository for that model and interface. Taking an example of modeling inventory for an Event we may have an interface and model like:


public class Inventory : ModelBase, IInventory
{
	public override string TableName
	{
		get { return "[Inventory].[Inventory]"; }
	}

	public int EventId { get; set; }

	public string Description { get; set; }

	public DateTime? Date { get; set; }

	public int? NumberOfTickets { get; set; }
}

public interface IInventory : IModelBase
{
	int EventId { get; set; }

	string Description { get; set; }

	DateTime? Date { get; set; }

	int? NumberOfTickets { get; set; }
}

As you can see in this example we specify a schema and table to show how we can provide this level of support. This example has also included a further abstraction by modelling the core details of any model and moving it into a ModelBase class:

public interface IModelBase : ITableKeyInformation
{
	int? Id { get; set; }
}

public abstract class ModelBase : IModelBase
{
    public int? Id { get; set; }
    
	public abstract string TableName { get; }

    public virtual string IdField { get { return "[Id]"; } }
}

Finally we are ready for the concrete implementation of our Inventory repository:

public class InventoryRepository : 
	BaseRepository<IInventory, Inventory>, IInventoryRepository
{

}

All the effort that has gone into describing our data layer pays off here - the concrete inventory repository has all of it's core functionality already provided by the repository base. We would only need to add domain specific methods such as retrieving the inventory for a specific event.

Taking it Further

Having our repository split like this gives us a useful springboard for future development. We could implement IValidatingObject on IModelBase with an abstract implementation in ModelBase to get full framework validation support through our application. We could add caching at the base repository level so that our core, common functions are all covered. It takes a little extra effort to create but the benefits are well worth it. Plus, we can now test with impunity swapping in mock repositories returning mock implementations of our data models and our business logic would never know.

C# , Patterns

Comments are Locked for this Post