Sergey Metlov
Sergey Metlov

Reputation: 26291

Repository implementation

I'm thinking about improving my current repository GetAll method implementation and wrote a code below. I tried to find ideas of such approach in the Google but had no success. So please review the code and help me answer a number of questions below. Here is simplified example:

class Service
{
    public void DoOperation()
    {
        // Let's say we need to retrieve users by criteria
        var items = UnitOfWork
            .Over<User>() // Get repository of User
            .GetAll()     // Start querying

            // If we need simple WHERE then use:
            .Where(x => x.Email == "[email protected]")

            // If we need more complex condition then use specification:
            .Using(new UserNameContains("John"))

            // Execute:
            .List();
    }
}

class UnitOfWork
{
    public Repository<T> Over<T>()
    {
        // Get from DI container or injected field
        return new Repository<T>();
    }
}

class Repository<T>
{
    public QueryWrapper<T> GetAll()
    {
        return new QueryWrapper<T>(Session.QueryOver<T>());
    }
}

class QueryWrapper<T>
{
    // Query from DB session. Init from constructor.
    private IQueryOver<T> _query;

    public QueryWrapper<T> Where(Expression<Func<T, bool>> expression)
    {
        _query = _query.Where(expression);
        return this;
    }

    public QueryWrapper<T> Using(Specification<T> spec)
    {
        var spec = new TSpec();
        _query = spec.Apply(_query);
        return this;
    }

    public IEnumerable<T> List()
    {
        return return _query.List();
    }
}

abstract class Specification<T>
{
    public abstract IQueryOver<T> Apply(IQueryOver<T> query);
}

class UserNameContains : Specification<User>
{
    // Init from constructor:
    private string _name;

    public override IQueryOver<User> Apply(IQueryOver<User> query)
    {
        return /* apply filter condition here */;
    }
}

So as a result we getting such benefits:

  1. No more need to create custom Repos. The single generic is pretty enough.
  2. Custom specification implementations now separated from data layer and testable.
  3. Easy-to-use from Service.
  4. We always able to extend QueryWrapper to support additional methods like OrderBy etc.
  5. And it is still unit-testable.

Could you please point me to leaks of my approach or provide your vision of the problem? Also links to existing articles would be great.

Upvotes: 3

Views: 156

Answers (1)

Jamie Ide
Jamie Ide

Reputation: 49251

My advice is to reduce your architecture to the bare minimum and justify to yourself the benefit of every added layer and abstraction. The minimum GetAll implementation is:

Session.Query<User>();

If you have restrictions that will be re-used you can add them as extension methods. It's simple enough to refactor to extension methods if you reusable code becomes apparent. Even then, for simple cases there is often little benefit to refactoring if you're only wrapping a Lambda expression as in this example.

public static IQueryable<User> UserNameContains(this IQueryable<User> queryable, string text)
{
    return queryable.Where(u => u.UserNameContains(text));
}

I don't find the repository pattern very useful but it can be a reasonable way to organize code. But I really dislike the generic repository pattern because it enforces a one repository per class rule. If I use a repository I like to group queries that are related to a root object (such as Project, ProjectStatus, etc.). Furthermore, there is no benefit to providing Get or GetAll methods in a generic repository because the ISession already provides those.

As for testability, tests of queries are always integration tests and in ASP.NET MVC I test the controllers or any free-standing query methods directly. I use repositories in a Windows Forms project for testing.

Upvotes: 1

Related Questions