So, I\'ve read all the Q&A\'s here on SO regarding the subject of whether or not to expose IQueryable to the rest of your project or not (see here, and here), and I\'ve ulti
hmm.. I solved this in many ways depending on the type of ORM i use.
The main idea is to have one repository base class and one query method that takes so many parameters indicating all possible where/orderby/expand|include/paging/etc options.
Here is a quick and dirty sample using LINQ to NHibernate (of course the entire repository should be implementation detail):
public class RepositoryBase
{
private ISession Session;
public RepositoryBase()
{
Session = SessionPlaceHolder.Session;
}
public TEntity[] GetPaged(IEnumerable>> filters,
IEnumerable>> relatedObjects,
IEnumerable>> orderCriterias,
IEnumerable>> descOrderCriterias,
int pageNumber, int pageSize, out int totalPages)
{
INHibernateQueryable nhQuery = Session.Linq();
if (relatedObjects != null)
foreach (var relatedObject in relatedObjects)
{
if (relatedObject == null) continue;
nhQuery = nhQuery.Expand(relatedObject);
}
IQueryable query = nhQuery;
if (filters != null)
foreach (var filter in filters)
{
if (filter == null) continue;
query = query.Where(filter);
}
bool pagingEnabled = pageSize > 0;
if (pagingEnabled)
totalPages = (int) Math.Ceiling((decimal) query.Count()/(decimal) pageSize);
else
totalPages = 1;
if (orderCriterias != null)
foreach (var orderCriteria in orderCriterias)
{
if (orderCriteria == null) continue;
query = query.OrderBy(orderCriteria);
}
if (descOrderCriterias != null)
foreach (var descOrderCriteria in descOrderCriterias)
{
if (descOrderCriteria == null) continue;
query = query.OrderByDescending(descOrderCriteria);
}
if (pagingEnabled)
query = query.Skip(pageSize*(pageNumber - 1)).Take(pageSize);
return query.ToArray();
}
}
Normally you'll want to add many chaining overloads as shortcuts when you don't need paging for example, etc..
Here is another dirty one. Sorry I'm not sure if I can expose the final ones. Those were drafts and are OK to show:
using Context = Project.Services.Repositories.EntityFrameworkContext;
using EntitiesContext = Project.Domain.DomainSpecificEntitiesContext;
namespace Project.Services.Repositories
{
public class EntityFrameworkRepository : IRepository
{
#region IRepository Members
public bool TryFindOne(Expression> filter, out T result)
{
result = Find(filter, null).FirstOrDefault();
return !Equals(result, default(T));
}
public T FindOne(Expression> filter)
{
T result;
if (TryFindOne(filter, out result))
return result;
return default(T);
}
public IList Find() where T : class, IEntityWithKey
{
int count;
return new List(Find(null, null, 0, 0, out count));
}
public IList Find(Expression> filter, Expression> sort)
{
int count;
return new List(Find(filter, sort, 0, 0, out count));
}
public IEnumerable Find(Expression> filter, Expression> sort, int pageSize,
int pageNumber, out int count)
{
return ExecuteQuery(filter, sort, pageSize, pageNumber, out count) ?? new T[] {};
}
public bool Save(T entity)
{
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
EntityKey key = context.CreateEntityKey(GetEntitySetName(entity.GetType()), entity);
object originalItem;
if (context.TryGetObjectByKey(key, out originalItem))
{
context.ApplyPropertyChanges(key.EntitySetName, entity);
}
else
{
context.AddObject(GetEntitySetName(entity.GetType()), entity);
//Attach(context, entity);
}
return context.SaveChanges() > 0;
}
public bool Delete(Expression> filter)
{
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
int numberOfObjectsFound = 0;
foreach (T entity in context.CreateQuery(GetEntitySetName(typeof (T))).Where(filter))
{
context.DeleteObject(entity);
++numberOfObjectsFound;
}
return context.SaveChanges() >= numberOfObjectsFound;
}
#endregion
protected IEnumerable ExecuteQuery(Expression> filter, Expression> sort,
int pageSize, int pageNumber,
out int count)
{
IEnumerable result;
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
ObjectQuery originalQuery = CreateQuery(context);
IQueryable query = originalQuery;
if (filter != null)
query = query.Where(filter);
if (sort != null)
query = query.OrderBy(sort);
if (pageSize > 0)
{
int pageIndex = pageNumber > 0 ? pageNumber - 1 : 0;
query = query.Skip(pageIndex).Take(pageSize);
count = query.Count();
}
else
count = -1;
result = ExecuteQuery(context, query);
//if no paging total count is count of the entire result set
if (count == -1) count = result.Count();
return result;
}
protected internal event Action EntitiesFound;
protected void OnEntitiesFound(ObjectContext context, params T[] entities)
{
if (EntitiesFound != null && entities != null && entities.Length > 0)
{
EntitiesFound(context, entities);
}
}
//Allowing room for system-specific-requirement extensibility
protected Action ItemsFound;
protected IEnumerable ExecuteQuery(ObjectContext context, IQueryable query)
{
IEnumerable result = null;
if (query is ObjectQuery)
{
var objectQuery = (ObjectQuery) query;
objectQuery.EnablePlanCaching = false;
objectQuery.MergeOption = MergeOption.PreserveChanges;
result = new List(objectQuery);
if (ItemsFound != null)
ItemsFound(result);
return result;
}
return result;
}
internal static RelationshipManager GetRelationshipManager(object entity)
{
var entityWithRelationships = entity as IEntityWithRelationships;
if (entityWithRelationships != null)
{
return entityWithRelationships.RelationshipManager;
}
return null;
}
protected ObjectQuery CreateQuery(ObjectContext context)
{
ObjectQuery query = context.CreateQuery(GetEntitySetName(typeof (T)));
query = this.AggregateEntities(query);
return query;
}
protected virtual ObjectQuery AggregateEntities(ObjectQuery query)
{
return query;
}
private static string GetEntitySetName(Type entityType)
{
return string.Format("{0}Set", entityType.Name);
}
}
public class EntityFrameworkContext
{
private const string CtxKey = "ctx";
private bool contextInitialized
{
get { return HttpContext.Current.Items[CtxKey] != null; }
}
public EntitiesContext Context
{
get
{
if (contextInitialized == false)
{
HttpContext.Current.Items[CtxKey] = new EntitiesContext(ConfigurationManager.ConnectionStrings["CoonectionStringName"].ToString());
}
return (EntitiesContext)HttpContext.Current.Items[CtxKey];
}
}
public void TrulyDispose()
{
if (contextInitialized)
{
Context.Dispose();
HttpContext.Current.Items[CtxKey] = null;
}
}
}
internal static class EntityFrameworkExtensions
{
internal static ObjectQuery Include(this ObjectQuery query,
Expression> propertyToInclude)
{
string include = string.Join(".", propertyToInclude.Body.ToString().Split('.').Skip(1).ToArray());
const string collectionsLinqProxy = ".First()";
include = include.Replace(collectionsLinqProxy, "");
return query.Include(include);
}
internal static string After(this string original, string search)
{
if (string.IsNullOrEmpty(original))
return string.Empty;
int index = original.IndexOf(search);
return original.Substring(index + search.Length);
}
}
}
In Conery's MVC Storefront he created another layer called the "Service" layer which received IQueryable results from the respository and was responsible for applying various filters.
In all cases nobody should be interacting with the repository directly except the services layer.
Most flexible thing is to let Services interact with Repository whatever way they want, same as in above code (yet through one single point -as in example also- to write DRY code and find a place for optimization).
However, the more right way in terms of common DDD patterns is to use the "Specification" pattern, where you encapsulate all your filters, etc in Variables (Class Members, in LINQ typically of delegate types). LINQ can take big optimization benefit out of this when you combine it with "Compiled queries". If you google the {Specification Pattern} and {LINQ Compiled Queries} you'll get closer to what I mean here.