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.