The Repository Pattern – Part 2

I previously wrote about using the Repository Pattern with Linq to Fluent NHibernate and MySQL. Since that time, I have had the opportunity to refine and extend this approach. I have also noticed quite a few hits on the old post and thought that sharing these refinements would be useful. If you haven’t read the first post, then go read that first…I’ll wait here.

Code

The Repository Interfaces

I refined the repository interfaces a bit to enable the keyed repository to have a generic key. Previously, I had coded an ‘IIntKeyedRepository’ and an ‘IGuidKeyedRepository’. This can be simplified a bit by having a second order generic interface:

public interface IReadOnlyRepository<TEntity> where TEntity:class
{
    IQueryable<TEntity> All();
    TEntity FindBy(Expression<Func<TEntity, bool>> expression);
    IQueryable<TEntity> FilterBy(Expression<Func<TEntity, bool>> expression);
}
public interface IRepository<TEntity> : IReadOnlyRepository<TEntity> where TEntity:class
{
    bool Add(TEntity entity);
    bool Add(IEnumerable<TEntity> items);
    bool Update(TEntity entity);
    bool Delete(TEntity entity);
    bool Delete(IEnumerable<TEntity> entities);
}
public interface IKeyedRepository<TKey, TEntity> : IRepository<TEntity>
    where TEntity : class, IKeyed<TKey>
{
    TEntity FindBy(TKey id);
}
public interface IKeyed<TKey>
{
    TKey Id { get; }
}

 

Now all of the keyed entities implement the ‘IKeyed’ interface and the repository is of type ‘IKeyedRepository’. This is a bit simpler and more generic.

 Repository Time Bombs

I previously had created an entity that looked like the following:

public class Truck
{
    public virtual int Id { get; private set; }
    public virtual string Name { get; set; }
    public virtual string Type { get; set; }
    public virtual string PlateNumber { get; set; }
    public virtual Driver Driver { get; private set; }
    public virtual IList<Location> Locations { get; private set; }
}

After some testing, I soon realized the ‘Locations’ property was a “time bomb” waiting to happen. Once the resulting list become large, accessing this property is very slow. This “return every thing in the list” query is not scalable and has been removed.

This does not mean that I have removed all the NHibernate relational mappings between my entities. I am using the following rules:

  1. If the relationship goes in the direction where only a single object is returned (the ‘one’ end of a ‘one-to-many’) the mapping is safe to keep.
  2. If the relationship goes in the direction where there is a guaranteed limited small number of items in the list the mapping is probably safe to keep.
  3. If the relationship can return an unbounded number of items (the ‘many’ end of the ‘one-to-many’) the mapping is not safe and is removed.

Entity Validation

A common task is to validation an entity. This validation may be done at a number of layers. I strongly believe that my entities (domain model) should be as anemic as possible. Implementing the validation logic in the entity goes against the single responsibility principle. I prefer to have my validation classes stand on their own using the following interface:

public interface IValidator<T>
{
    bool IsValid(T entity);
    IEnumerable<string> BrokenRules(T entity);
}

To create a concrete example, let’s say we have a ‘User’ entity defined by the following class:

public class User : IKeyed<int>
{
    public virtual int Id { get; private set; }
    public virtual string UserName { get; set; }
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual string Email { get; set; }
}

Then the validator for this entity will look something like the following:

public class UserValidator : IValidator<User>
{
    private readonly IKeyedRepository<int, User> _userRepo;
    public UserValidator(IKeyedRepository<int, User> userRepo)
    {
        _userRepo = userRepo;
    }

    public bool IsValid(User entity)
    {
        return BrokenRules(entity).Count() > 0;
    }

    public IEnumerable<string> BrokenRules(User entity)
    {
        if(string.IsNullOrEmpty(entity.UserName))
        {
            yield return "User name is required.";
        }
        else
        {
            if(_userRepo.FindBy(x=>x.UserName==entity.UserName)!=null)
            {
                yield return "User name is already taken.";
            }
        }
        if(string.IsNullOrEmpty(entity.FirstName))
        {
            yield return "First name is required.";
        }
        if(string.IsNullOrEmpty(entity.LastName))
        {
            yield return "Last name is required.";
        }
        if(string.IsNullOrEmpty(entity.Email))
        {
            yield return "Email address is required.";
        }
        else
        {
            const string patternStrict = @"^(([^<>()[].,;:s@""]+"
                                         + @"(.[^<>()[].,;:s@""]+)*)|("".+""))@"
                                         + @"(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}"
                                         + @".[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+"
                                         + @"[a-zA-Z]{2,}))$";
            Regex regexStrict = new Regex(patternStrict);
            if(!regexStrict.IsMatch(entity.Email))
            {
                yield return "Email must be valid format.";
            }
        }

        yield break;
    }
}

This validator takes as a constructor parameter a reference to the ‘IKeyedRepository’ for the ‘User’ entities. This is used to ensure the ‘UserName’ property is unique in the database. The rest of the validation ensures the require properties exist and are in the expected format.

Service Wrapper

Passing around your generic ‘IRepository’ instances maybe fine for small applications. However, you soon end up with MVC controllers constructors that look like the following:

public HomeController(  ILogger logger,
                        IKeyedRepository<int, User> userRepo,
                        IKeyedRepository<int, Truck> truckRepo,
                        IKeyedRepository<int, Location> locationRepo,
                        IUnitOfWork unitOfWork)
{
    _logger = logger;
    _userRepo = userRepo;
    _truckRepo = truckRepo;
    _locationRepo = locationRepo;
}

Even though we are using a IOC container to manage the life-time of these parameters, this is quickly becoming un-manageable.

Also, there usually exist cross-cutting queries that a single repository cannot handle. For instance find the current location of a truck given a user. Implementing these queries in higher level (presentation or business logic) layers can cause issues. This also usually ends up violating the DRY principle as the queries are replicated in various places. These queries need to have a home.

For this reason, it is a good idea to create a service layer around the generic repository implementations. Assume that we have the following implemented using our favorite ORM:

public class Repository<T> : IKeyedRepository<int, T>
    where T : class, IKeyed<int>
{
    // implementation goes here...
}

 

The goal is to create a service layer that provides access to the desired repositories and the implementation of cross-cutting queries. Here is an example:

public class DataService
{
    private readonly IKeyedRepository<int, User> _userRepo;
    private readonly IKeyedRepository<int, Truck> _truckRepo;
    private readonly IKeyedRepository<int, Location> _locationRepo;
    private readonly IUnitOfWork _unitOfWork;

    public UserService Users { get; private set; }
    public TruckService Trucks { get; private set; }
    public LocationService Locations { get; private set; }

    public DataService(IKeyedRepository<int, User> userRepo,
        IKeyedRepository<int, Truck> truckRepo,
        IKeyedRepository<int, Location> locationRepo,
        IUnitOfWork unitOfWork)
    {
        _userRepo = userRepo;
        _truckRepo = truckRepo;
        _locationRepo = locationRepo;
        _unitOfWork = unitOfWork;

        Users = new UserService(userRepo);
        Trucks = new TruckService(truckRepo);
        Locations = new LocationService(locationRepo);
    }

    public Location GetCurrentLocation(int truckId)
    {
        return _locationRepo
            .FilterBy(x => x.Id == truckId)
            .OrderByDescending(c => c.Timestamp)
            .FirstOrDefault();
    }

    public void Commit()
    {
        _unitOfWork.Commit();
    }
}

The ‘DataService’ class now accepts most of the parameters that were previously injected into the ‘HomeController’. So what have we gained? Since we probably have more than one MVC controller and those other controllers probably had the same large list of constructor parameters we have simplified our controllers and increased the maintainability of our application.

The ‘DataService’ exposes three properties (‘Users’, ‘Trucks’ and ‘Locations’), one additional query (‘GetCurrentLocation’) and the ‘Commit’ method. The additional query is not cross cutting, but you could easily imagine one that did have cross-cutting concerns. The following is an example of one of the properties (the others in the download):

public class UserService
{
    private readonly IKeyedRepository<int, User> _userRepo;
    private readonly UserValidator _validation;

    public UserService(IKeyedRepository<int, User> userRepo)
    {
        _userRepo = userRepo;
        _validation = new UserValidator(userRepo);
    }
    public IList<User> All()
    {
        return _userRepo.All().ToList();
    }

    public User FindBy(Expression<Func<User, bool>> expression)
    {
        return _userRepo.FindBy(expression);
    }

    public IList<User> FilterBy(Expression<Func<User, bool>> expression)
    {
        return _userRepo.FilterBy(expression).ToList();
    }

    public bool Add(User entity, out IEnumerable<string> brokenRules)
    {
        if(!_validation.IsValid(entity))
        {
            brokenRules = _validation.BrokenRules(entity);
            return false;
        }
        brokenRules = null;
        return _userRepo.Add(entity);
    }

    public bool Add(IEnumerable<User> items, out IEnumerable<string> brokenRules)
    {
        foreach (User item in items)
        {
            if(!_validation.IsValid(item))
            {
                brokenRules = _validation.BrokenRules(item);
                return false;
            }
        }
        brokenRules = null;
        return _userRepo.Add(items);
    }

    public bool Update(User entity, out IEnumerable<string> brokenRules)
    {
        if(!_validation.IsValid(entity))
        {
            brokenRules = _validation.BrokenRules(entity);
            return false;
        }
        brokenRules = null;
        return _userRepo.Update(entity);
    }

    public bool Delete(User entity)
    {
        return _userRepo.Delete(entity);
    }

    public bool Delete(IEnumerable<User> entities)
    {
        return _userRepo.Delete(entities);
    }

    public User FindBy(int id)
    {
        return _userRepo.FindBy(id);
    }
}

The ‘UserService’ essentially implements the ‘IKeyedRepository<int, User>’ interface with two small (but important) differences.

  1. All the methods in ‘IKeyedRepository’ that return ‘IQueryable’ now return ‘IList’. This means that the power of LINQ to build up queries stops in this layer. This is an important architecture decision. Higher layers are not going to be able to filter the query results further. The reason for this choice is that we want to force all ‘querying’, ‘filtering’ or ‘paging’ code down into this service layer. This help enforce the DRYness of our application.
  2. Validation logic has been added to the ‘Add’ and ‘Update’ methods. This validation logic is provided by ‘IValidator’ implementations.

Summary

The above refinements and extensions to the previously discussed repository pattern coalesce into a pattern that I have found useful. I hope this additional information is useful to you. If you have additional refinements or suggestions, please feel free to share with the other readers.

Comments
  1. Samuel Fraser
  2. Ted Krumbach
  3. Ron Tedwater
  4. Alberto B
    • rcravens
      • Alberto B
  5. Haroon
  6. Matt
  7. Andy
    • rcravens
  8. Bostjan
  9. Jean Carlos

Leave a Reply

Your email address will not be published. Required fields are marked *

*