Need Quality Code? Get Silver Backed

Repositories - Removing Complexity

14thMay

0

by Gary H

Fools ignore complexity. Pragmatists suffer it. Some can avoid it. Geniuses remove it.

Alan Perlis

In our last article we looked at the repository pattern in practice. We came up with a solution that was based on some assumptions specifically around ID's being ints and we implemented a simple base repository that was fairly flexible and generic but required a fair amount of configuration upfront in your code.

That solution was also rather complex with some duplicate code and didn't handle a number of common problems cleanly for example - what about connection sharing or transactions? In this post we'll take a look at refactoring our previous pattern to carve out as much complexity as we can and hopefully end up with something that's even more flexible than what we started with.

Where to Start

Our flexibility is related in some measure to our tools. In this case we will add another weapon to our arsenal - Dapper Extensions. These extensions build upon Dapper to add common functionality such as Get, Insert, Update and Delete to the base IDbConnection. By making use of this we can start to pare down our base repository. We will begin by eliminating the ITableKeyInformation interface completely and tidying up our base repo' signature to take this into account:

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

Next we'll also tidy up our ModelBase class to reflect this:

public interface IModelBase
{
	Guid? Id { get; set; }
}

public abstract class ModelBase : IModelBase
{
	public Guid? Id { get; set; }
}

Just to mix things up a bit we'll also move this example to having a Guid as the Primary Key to show off Dapper Extensions' ability to use int or guid data types out of the box.

Eliminating Repetition

With our base model updated we'll also introduce a wrapping method to take care of all of our tedious code. We want to allow all of our core methods to be able to act on an existing IDbConnection so that we can correctly support transactions across many repositories. This will be an optional feature though so we should still create a new connection if we are not supplied one.

protected TOut WrappedDbAction<TOut>(IDbConnection existingConnection, 
								Func<IDbConnection, TOut> actionToWrap)
{
	var conn = existingConnection ?? CreateConnection();
	try
	{
		return actionToWrap(conn);
	}
	finally
	{
		if (existingConnection == null)
		{
			conn.Close();
			conn.Dispose();
		}
	}
}

Here we have an existing connection passed in, if it's null we will create a new connection. We then call the function passed in by the caller, this expression will return a TOut and take in a connection, specifically either the existing connection or the new one we have created. Once this call is completed we will dispose of our connection if it is one we have created ourselves.

The final step is to put this all together into our base repository:

public abstract class BaseRepository<TInterface, TConcreteClass> : 
	IBaseRepository<TInterface>
		where TConcreteClass : class, TInterface, new()
		where TInterface : IModelBase
{
	public virtual TInterface Get<TId>(T
		Id id, 
		IDbConnection existingConnection = null)
	{
		return WrappedDbAction(existingConnection, 
			conn => conn.Get<TConcreteClass>(id));
	}

	public virtual bool Delete(TInterface entity, 
		IDbConnection existingConnection = null, 
		IDbTransaction transaction = null)
	{
		return WrappedDbAction(existingConnection, 
			conn => conn.Delete((TConcreteClass)entity, transaction));
	}

	public virtual bool Delete(object predicate, 
		IDbConnection existingConnection = null, 
		IDbTransaction transaction = null)
	{
		return WrappedDbAction(existingConnection, 
			conn => 
				conn.Delete<TConcreteClass>(predicate, transaction));
	}

	public virtual IEnumerable<TInterface> All(
		IDbConnection existingConnection = null)
	{
		return All(null, existingConnection);
	}

	public virtual IEnumerable<TInterface> All(
		object predicate, IDbConnection existingConnection = null)
	{
		return WrappedDbAction(existingConnection, 
			conn => conn.GetList<TConcreteClass>(predicate));
	}

	public virtual Guid Save(TInterface source, 
		IDbConnection existingConnection = null, 
		IDbTransaction transaction = null)
	{
		return WrappedDbAction(existingConnection, conn =>
    	{
			if (!source.Id.HasValue)
			{
				return (Guid)conn.Insert((TConcreteClass)source, 
											transaction);
			}

			conn.Update((TConcreteClass)source, transaction);
			return source.Id.Value;
    	});
	}

	protected TOut WrappedDbAction<TOut>(
		IDbConnection existingConnection, 
		Func<IDbConnection, TOut> actionToWrap)
	{
		var conn = existingConnection ?? CreateConnection();
		try
		{
			return actionToWrap(conn);
		}
		finally
		{
			if (existingConnection == null)
			{
				conn.Close();
				conn.Dispose();
			}
		}
	}

	protected IDbTransaction CreateTransaction(
		IDbConnection connection, 
		IsolationLevel level = IsolationLevel.Unspecified)
	{
		return connection.BeginTransaction(level);
	}

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

Those calls to predicates allow detailed filtering of our result set on the database server itself. For a full breakdown on how they work take a look at the Dapper Extensions Predicate documentation.

Irregular Data Sources

If your models do not map exactly onto your database, all is not lost! Dapper and DapperExtensions both provide means for mapping your existing data structure into clean POCO classes.

Dapper Extensions brings the Auto Class Mapper. This allows you to manually map your data source to your class in code. Be aware that your custom mappings need to live in public classes in the same assembly as your models - Dapper Extensions will scan only this assembly (and only once). This is arguably the cleanest method.

Dapper itself provides the CustomPropertyTypeMap which has similar functionality but more verbose syntax. Stack Overflow has a good post with more details

C# , Patterns

Comments are Locked for this Post