Code Review Stack Exchange is a question and answer site for peer programmer code reviews. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

I've been toying around making my repositories a bit more fluent for a while. I was ending up with crazy long method names when I had to query on multiple conditions. So I've been working on a way to query fluently.

Usage

Insert/Update/Delete

Looks and behaves exactly like a normal repository.

var personRepository = new PersonRepository(dbContext);

personRepository.Insert(new Person {...});
personRepository.Update(person);
personRepository.Remove(person);

Querying

Allows you to call any query methods, narrowing down the query more and more as you go and can execute the query synchronously or asynchronously.

var person = await personRepository.Query()
    .ByFirstName("neil")
    .ByLastName("smith")
    .Include(m => m.Addresses)
    .OrderBy(m => m.LastName)
    .Take(5)
    .ToEntitiesAsync();

var person = personRepository.Query()
    .ById(99)
    .ToEntity();

var person = await personRepository.Query()
    .WhereFirstNameContains("jr")
    .ToEntitiesAsync();

So the code behind this:

Entity Base Class

public abstract class BaseEntity
{
    public int Id { get; set; }
    public DateTime DateCreated { get; set; }
}

Repository Contracts

public interface IPersistableRepository<TEntity> where TEntity : BaseEntity
{
    void Insert(TEntity entity);
    void Update(TEntity entity);
    void Remove(TEntity entity);
}

public interface IQueryableRepository<TEntity, out TQueryBuilder>
    where TEntity : BaseEntity
    where TQueryBuilder : class, IQueryBuilder<TEntity, TQueryBuilder>    
{
    TQueryBuilder Query();
}

public interface IQueryBuilder<TEntity, out TQueryBuilder>
    where TEntity : BaseEntity
    where TQueryBuilder : class
{
    TQueryBuilder ById(int id);
    ...

    TQueryBuilder Include<T>(Expression<Func<TEntity, T>> prop);
    TQueryBuilder OrderBy<T>(Expression<Func<TEntity, T>> prop);
    TQueryBuilder OrderByDescending<T>(Expression<Func<TEntity, T>> prop);
    TQueryBuilder Take(int count);
    TQueryBuilder After(int id);
    TQueryBuilder Before(int id);

    TEntity ToEntity();
    Task<TEntity> ToEntityAsync();
    IEnumerable<TEntity> ToEntities();
    Task<IEnumerable<TEntity>> ToEntitiesAsync();
}

Repository Base Classes

public abstract class PersistableRepository<TEntity> : IPersistableRepository<TEntity>
    where TEntity : BaseEntity
{
    private readonly IDbContext _context;

    protected PersistableRepository(IDbContext context) {
        _context = context;
    }

    protected IDbSet<TEntity> DbSet { 
        get { return _context.Set<TEntity>(); }
    }

    public void Insert(TEntity entity) {
        DbSet.Add(entity);
    }

    public void Update(TEntity entity) {
        _context.Entry(entity).State = EntityState.Modified;
    }

    public void Remove(TEntity entity) {
        DbSet.Remove(entity);
    }
}

public abstract class QueryBuilder<TEntity, TQueryBuilder> 
    : IQueryBuilder<TEntity, TQueryBuilder>
    where TEntity : BaseEntity
    where TQueryBuilder : class
{
    protected QueryBuilder(IQueryable<TEntity> query) {
        Query = query;
    }

    protected IQueryable<TEntity> Query { get; set; }

    public TQueryBuilder ById(int id) {
        Query = Query.Where(m => m.Id == id);
        return this as TQueryBuilder;
    }

    public TQueryBuilder Include<T>(Expression<Func<TEntity, T>> prop) {
        Query = Query.Include(prop);
        return this as TQueryBuilder;
    }

    public TQueryBuilder OrderBy<T>(Expression<Func<TEntity, T>> prop) {
        Query = Query.OrderBy(prop);
        return this as TQueryBuilder;
    }

    public TQueryBuilder OrderByDescending<T>(Expression<Func<TEntity, T>> prop) {
        Query = Query.OrderByDescending(prop);
        return this as TQueryBuilder;
    }

    public TQueryBuilder Take(int count) {
        Query = Query.Take(count);
        return this as TQueryBuilder;
    }

    public TQueryBuilder After(int id) {
        Query = Query.Where(m => m.Id >= id);
        return this as TQueryBuilder;
    }

    public TQueryBuilder Before(int id) {
        Query = Query.Where(m => m.Id <= id);
        return this as TQueryBuilder;
    }

    public TEntity ToEntity() {
        return Query.FirstOrDefault();
    }

    public async Task<TEntity> ToEntityAsync() {
        return await Query.FirstOrDefaultAsync();
    }

    public IEnumerable<TEntity> ToEntities() {
        return Query.ToList();
    }

    public async Task<IEnumerable<TEntity>> ToEntitiesAsync() {
        return await Query.ToListAsync();
    }
}

Repository & QueryBuilder Interfaces

public interface IPersonRepository
    : IPersistableRepository<Person>, IQueryableRepository<Person, IPersonQueryBuilder>
{
}

public interface IPersonQueryBuilder
    : IQueryBuilder<Person, IPersonQueryBuilder>
{
    IPersonQueryBuilder ByFirstName(string firstName);
    IPersonQueryBuilder ByLastName(string lastName);
    IPersonQueryBuilder ByAge(int age);
    IPersonQueryBuilder WhereFirstNameContains(string val);
    ...
}

Repository Implementation

public class PersonRepository : PersistableRepository<Cohort>, IPersonRepository
{
    public PersonRepository(IDbContext context) : base(context)
    {
    }

    public IPersonQueryBuilder Query() {
        return new PersonQueryBuilder(DbSet.AsQueryable());
    }
}

public class PersonQueryBuilder
    : QueryBuilder<Person, IPersonQueryBuilder>, IPersonQueryBuilder
{
    public PersonQueryBuilder(IQueryable<Person> query) : base(query)
    {
    }

    public IPersonQueryBuilder ByFirstName(string firstName) {
        Query = Query.Where(m => m.FirstName == firstName);
        return this;
    }

    public IPersonQueryBuilder ByLastName(string lastName) {
        Query = Query.Where(m => m.LastName == lastName);
        return this;
    }

    public IPersonQueryBuilder ByAge(int age) {
        Query = Query.Where(m => m.Age == age);
        return this;
    }
}

Everything has it's place. Query methods shared by all entity classes belong to the abstract QueryBuilder. Methods used to persist data belong to the abstract PerstiableRepository. All Repository and QueryBuilder methods are enforced by an interface so it's inject-able and testable. And the ability to use whatever combination of query methods give it a better query syntax I believe. Each repository can implement either IPersistableRepository, IQueryableRepository, or both.

But I'm not an expert. Am I missing anything? Should I not do this this way? You tell me what's wrong with my design.

share|improve this question
    
Post rolled back. Please do not update the original code based on answers as that will invalidate them. – Jamal Jun 15 '14 at 22:26
    
In this case, with only one answer, the edit wouldn't invalidate anything. See the point when there's multiple answers. No big deal though, the idea is the same with or without the edit. – Smith.h.Neil Jun 15 '14 at 22:36
up vote 2 down vote accepted

I quite like the concept. The only thing that stands out for me initially is that I would probably leave the wrapping methods the same as their wrapped name.

i.e.

TEntity FirstOrDefault()
Task<TEntity> FirstOrDefaultAsync()
IEnumerable<TEntity> All()
Task<IEnumerable<TEntity>> AsEnumerable();

rather than

TEntity ToEntity();
Task<TEntity> ToEntityAsync();
IEnumerable<TEntity> ToEntities();
Task<IEnumerable<TEntity>> ToEntitiesAsync();

I guess my main reasoning is that FirstOrDefault makes it clear what we are retrieving where as ToEntity() leaves it open to thought between Single() and First().

Neat idea thought. Be interested to see what others think.

share|improve this answer
    
Can't imagine why I decided to change those method names. Makes a lot of sense to keep in consistent and will make a lot more sense if I want to add Find, Single, etc... Thanks for the input. Going to edit my question with this change in a few. – Smith.h.Neil Jun 15 '14 at 22:18

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.