A Truly Generic Repository, Part 2

This is part of a series on using generics in C# to make code more resuable. Other articles in this series:

In the first part of this article, we looked at creating a truly generic repository that can work with any entity without having to new up multiple instances. In this second part, we'll discuss how we can extend this to generically cover additional scenarios, as well as what to do when you truly need some method specifically targeting a single entity.

If you read the first part of this series, Generic Entity Base Class, you'll recall the PublicationEntity<T> class I gave there. If you haven't read that, yet, this might be a good time to give it a look. Simply, PublicationEntity<T> extended our Entity<T> class, adding some properties that deal with the "published" state of an entity: Status, PublishDate, and ExpireDate. It also implemented an interface, IPublicationEntity, which will be important to our discussion here.

Generics 101

I kind of threw you head-first into the world of generics, without much of a paddle. As a result, it's probably best if I back up a bit and explain a little about how they work. Basically, generics are kind of like a placeholder. You're telling the compiler, "I know I'll need some type here, but I'm not sure what that is right now. Once I do know, I'll let you know, as well." Going back to our Entity<T> example, we had the following property:

public T Id { get; set; }

What we're saying here is that we don't know right now what T is, so we'll pass it in later, when we do. The way we do that is with the angle brackets syntax. These are called our type parameters. Entity<T> has a single type parameter, T, so in order to instantiate Entity<T> we'll have to pass something for that. If I were do something like the following in my application:

var entity = new Entity<int>();

// Note: Entity<T> is abstract, so this doesn't actually work.
// It's just for illustration.

The Id property becomes as if we statically typed it as:

public int Id { get; set; }

An important concept of generics is constraints. Constraints allow us to limit the parameter to a subset of types. You add a constraint, using where. You saw this in the code in part one. For example:

public TEntity FindById(object id)
    where TEntity : class, IEntity

This tells the compiler that the TEntity type parameter must be a class and it must have the IEntity interface. As a result, an exception will now be raised if you pass something like int, since that's a struct, not a class. You would also get an exception if you passed a class that doesn't implement IEntity. However, more important than telling the compiler what cannot be used as a type parameter, it tells the compiler what features the generic type will have. We can now freely interact with anything defined on IEntity within the method, because we know that any instance of TEntity will be something that implements IEntity.

Extending the Repository with Interfaces

Therefore, given our IPublicationEntity interface, we can create methods that specifically interact with properties any class that implements IPublicationEntity are guaranteed to have. For example:

TEntity GetOneLive<TEntity>(
    Expression<Func<TEntity, bool>> filter = null,
    string includeProperties = null)
    where TEntity : class, IPublicationEntity;

You'll notice this signature looks exactly like GetOne<TEntity> from IReadOnlyRepository, except that we added "Live" to the name and used IPublicationEntity as a constraint, rather than IEntity. Since, we're guaranteed to be working with entities that implement IPublicationEntity in this method, we can freely utilize the properties on that interface, Status, PublishDate, and ExpireDate, within the method. Let's look at the implementation:

protected virtual IQueryable<TEntity> GetLiveQueryable<TEntity>(
    Expression<Func<TEntity, bool>> filter = null,
    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
    string includeProperties = null,
    int? skip = null,
    int? take = null)
    where TEntity : class, IPublicationEntity
{
    var query = GetQueryable<TEntity>(filter, orderBy, includeProperties);

    query = query.Where(m => m.Status == PublishStatus.Published &&
                             m.PublishDate.HasValue &&
                             m.PublishDate <= DateTime.UtcNow &&
                             (!m.ExpireDate.HasValue || m.ExpireDate > DateTime.UtcNow));

    if (skip.HasValue)
    {
        query = query.Skip(skip.Value);
    }

    if (take.HasValue)
    {
        query = query.Take(take.Value);
    }

    return query;
}

...

public virtual TEntity GetOneLive<TEntity>(
    Expression<Func<TEntity, bool>> filter = null,
    string includeProperties = null)
    where TEntity : class, IPublicationEntity
{
    return GetLiveQueryable<TEntity>(filter, null, includeProperties).SingleOrDefault();
}

First, I've added a new protected method, GetLiveQueryable. This is similar in function to the previous GetQueryable method we had already. In fact, it uses this method internally. However, it goes further by adding an additional where clause that defines what we consider "live": status set to "Published", publish date that's in the past, and either no expire date or a future expire date.

You'll notice that although I could have passed the skip and take parameters to GetQueryable as well, I've instead repeated the code from GetQueryable for that, here. The reason for that is simple. Skip and Take will directly act on our queryable. If we apply the "live" logic afterward, it will take effect after it's already been limited. To ensure that we skip/take the appropriate amount of items, we need to call that after all the where clause is done.

The GetOneLive method, then, is remarkably similar to GetOne: the only difference being that it calls GetLiveQueryable rather than GetQueryable. So, all we're doing is adding an additional layer of logic here, which of course is only possible because of the IPublicationEntity constraint. You can then rinse and repeat for all the other methods in the repository, creating "Live" versions of each.

Through the use of interfaces, generics and smart type parameter constraints you can create infinitely complex iterations of this idea. Whenever you have some group of entities that share common characteristics, define those commonalities in an interface that each will then implement. Then, you can add more generic methods to your repository to target scenarios that those common characteristics present.

Handling Entity-Specific Scenarios

What if there's something that only truly applies to one entity, though? What should you do in that circumstance. My personal recommendation here is to use partial classes and additional interfaces. Let's look at a simple example:

public class Cat : Entity<int>
{
    public bool IsLongHaired { get; set; }
}

public ICatRepository
{
    public IEnumerable<Cat> GetLongHairedCats();
}

Then, we just modify our IReadOnlyRepository interface and EntityFrameworkReadOnlyRepository class a bit:

public interface IReadOnlyRespository : ICatRepository
{
    ...
}

public partial class EntityFrameworkReadOnlyRepository<TContext> : IReadOnlyRepository
    where TContext : DbContext
{
    ...
}

Simply, we just added the ICatRepository as a base to our IReadOnlyRepository. In C# you can only inherit from one class, but you can implement any number of interfaces. Therefore, as the need arises you can continue to add additional interfaces in this way. Then, we added the partial keyword to our EntityFrameworkReadOnlyRepository class. This isn't strictly necessary. You always just add the GetLongHairedCats method directly here, but I prefer to segrate the generic code from the non-generic code. This keeps things more tidy and also lets you more easily get the code you need should do quite of bit of this type of customization. The partial keyword, if you aren't familiar, tells the compiler, simply, that the class is broken into multiple files, so it should collect all instances and combine them together. As far as your application is concerned, there's just one EntityFrameworkReadOnlyRepository and it has all the methods on it, but in your project it might be composed from many different locations.

Now, with that, lets add our implementation:

CatEntityFrameworkReadOnlyRepository.cs

public partial class EntityFrameworkReadOnlyRepository<TContext> : IReadOnlyRepository
    where TContext : DbContext
{
    public IEnumerable<Cat> GetLongHairedCats()
    {
        return GetQueryable<Cat>(m => m.IsLongHaired).ToList();
    }
}

Conclusion

I hope you can now see how powerful the combination of interfaces and generics can be. Where before you might have some monster Unit of Work class with a tens or hundreds of properties each holding a separate instance of a repository pertaining to a particular entity type, you have now just one relatively tidy and straight-forward class that can interact with any given entity class. Since it implements an interface, which acts a contract, you can create many different implementations and use dependency injection to inject just the right one for your application. Also, since it's truly generic, end-to-end, you can utilize this not just in one specific application but rather in many different applications. You could put all this in a class library and add that as a dependency to each project.

In the next couple of articles in this series, we'll look at a practical application of our generic repository and see how we can further use generics to make powerful framework for creating and administration interface for our entities.

Addendum: Caveat Emptor

I wanted to keep the code here as simple as possible, which is hard to do with complex ideas like this. However, I felt I would be remiss if I left you here without a little practical guidance concerning using partial classes and adding additional repository interfaces. You might have noticed that I violated the open-closed principle pretty blatantly in my discussing of this matter, and since I hinted at making a reusable class library, that's a pretty big deal. If I was actually doing this in my own application, I would create a separate interface/implementation just for my application. For example:

public interface IApplicationRepository : IRepository
{
}

public partial class ApplicationEntityFrameworkRepository
    : EntityFrameworkRepository<ApplicationDbContext>, IApplicationRepository
{
    public ApplicationEntityFrameworkRepository(ApplicationDbContext context)
        : base(context)
    {
    }
}

Now, with these, you can extend to your heart's content adding things not only specific to entities but to the particular application as well. You would just add an additional base interface to IApplicationRepository, as necessary and then create a new partial ApplicationEntityFrameworkRespository class to implement it. That way, you're not violating open-closed, and you keep your generic repository, well, generic, such that it can be reused without having the port around additional interfaces.

comments powered by Disqus