Let it be said that I strongly dislike code duplication. For that matter, I also strongly dislike repetitive typing. Yet as I started to build out DevEvents, I found myself frequently duplicating similar code across object repositories, and typing the same base statements. In these cases, I typically create a base class to take care of the duplicated functionality, and inherit the dependant classes from the base. With the object repositories however, each duplicated section of code was slightly dependant on the object type. Not to be deterred, I ended up creating a custom generics-based repository implementation that virtually eliminated the code duplication. But let's start from the beginning.
DevEvents is built on top of a LINQ to SQL model which lives in its own assembly. That assembly is then referenced from a Components layer in which the repositories live. A typical repository implementation might look like this:
public class OldLogRepository : IOldLogRepository
{
private DEDataContext db;
public OldLogRepository()
{
db = new DEDataContext();
}
internal OldLogRepository(DEDataContext context)
{
db = context;
}
public int GetLogEntryCount()
{
return db.Logs.Count();
}
public void ClearLogEntriesBefore(DateTime dateTime)
{
var query = (from le in db.Logs
where le.EventDate <= dateTime
select le);
db.Logs.DeleteAllOnSubmit(query);
}
}
I feel this is a pretty standard repository implementation, and it helps promote loose coupling at several levels. First, the repository itself is interface-based which means it can be swapped out if necessary. Second, the internal constructor allows for dependency injection of a non-default data context.
My frustration arose when I had to duplicate a lot of the standard functionality across dozens of repositories, such as Count, Create, Delete, Update, Select, etc. The first idea was to create a base class to house that functionality, but as shown in the ClearLogEntriesBefore method above, there was some object specific logic in the methods. I realized however, that the method could be broken up into different parts: the query, the context, and the table. A quick check of the model code confirmed this, and I set about to create a generic repository implementation to reduce or eliminate the amount of redundant code. I started with an abstract generic class:
public abstract class BaseRepository<TModel> : IBaseRepository<TModel>
Next, I created the core fields and constructor:
protected DEDataContext db;
protected Table<TModel> _table;
public BaseRepository(Table<TModel> table)
{
db = new DEDataContext();
_table = table;
}
Note that the default constructor tables a parameter of type System.Data.Linq.Table<TEntity>. This is the base type of all entity objects, and allows the base class to handle generic table operations on the correct table.
So far I have taken care of the context and the table parts of my original duplicated methods. Next, I needed to solve the query problem. When I examined the method signature for operations such as Where and Select, I discovered that they all took a parameter of type Expression<Func<TModel, bool>>. What this means is that it is possible to pass parameters as a known base type. The base Contains method could now be written as:
public bool Contains(Expression<Func<TModel, bool>> predicate)
{
return _table.Any(predicate);
}
From the top level repository class, I could then call Contains as follows:
return Contains(le => le.SomeProperty == someValue);
Using this methodology I was able to create a number of base methods, such as Count. The next step was basic CRUD operations; Create, Select, Update and Delete.
Create is fairly straightforward:
public void Create(TModel model, bool save)
{
_table.InsertOnSubmit(model);
if (save)
_table.Context.SubmitChanges();
}
public void Create(IEnumerable<TModel> models, bool save)
{
_table.InsertAllOnSubmit(models);
if (save)
_table.Context.SubmitChanges();
}
You might be wondering about the save parameter. In LINQ to SQL, entities added to the context are not saved to the database until SubmitChanges() is called. There are certain instances in the application where it is preferable to add a group of entities, and then commit them to the database.
Delete and select got a little more complicated, since I am optionally passing a predicate of type Expression<Func<TModel, bool>>. The answer lies in the fact that IQueryable types are just that until they are invoked, a query. This means that I can refine the query, such as is done in the Delete method:
protected void Delete(Expression<Func<TModel, bool>> predicate)
{
var list = (from e in _table
select e);
if (predicate != null)
list = list.Where(predicate);
_table.DeleteAllOnSubmit(list);
_table.Context.SubmitChanges();
}
You will notice that the Delete method is marked as Protected. This is to prevent it from being called outside of the repository, thereby enforcing interaction with the top-level repository methods to perform repository operations. For that matter, the db field in BaseRepository should be marked as private to ensure it is the single point of responsibility for the data context, but there is some cleanup to do, and I digress.
Select is similar, except that it is implemented across multiple methods: LoadByQuery, LoadAllByQuery, LoadTopByQuery and LoadAll, all of which have overrides to optionally take a DataLoadOptions object. There are also a couple specialized methods to pull data in a serializable format. LoadAllByQuery is shown here implementing query refinements:
protected IQueryable<TModel> LoadAllByQuery(Expression<Func<TModel, bool>> predicate)
{
return LoadAllByQuery(predicate, null);
}
protected IQueryable<TModel> LoadAllByQuery(Expression<Func<TModel, bool>> predicate, DataLoadOptions dlo)
{
if (dlo != null)
db.LoadOptions = dlo;
var list = (from e in _table
select e);
if (predicate != null)
list = list.Where(predicate);
return list;
}
With the generics-based repository implementation in place, I can now rewrite the original LogRepository as follows:
public sealed class LogRepository : BaseRepository<Log>, ILogRepository
{
public LogRepository()
: base(new DEDataContext().Logs)
{
}
public void ClearLogEntriesBefore(DateTime dateTime)
{
Delete(l => l.EventDate < dateTime);
}
}
First, you will notice that there are significantly fewer lines of code (eleven versus twenty-three), and what is there is much less complex. Second, the Count method is no longer included. Since the Count method is in the base class and marked as Public, it can be called directly from the instantiated object.
Here is another method from the Log repository, made simpler through its interaction with the base methods:
public IQueryable<Log> GetLogEventsPaged(int pageID)
{
return LoadAllByQuery(null)
.Skip(((pageID - 1) * DEContext.Current.PageSize))
.Take(DEContext.Current.PageSize);
}
I have found a generics-based repository to be incredibly useful and efficient for my purpose. So my question to you, dear reader, is what do you think? How would you improve upon this implementation to make it better?