7
\$\begingroup\$

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.

\$\endgroup\$
2
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ 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. \$\endgroup\$ – Smith.h.Neil Jun 15 '14 at 22:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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