linq-to-sql - unit - repositorios en c#
¿Cómo puedo escribir un repositorio limpio sin exponer IQueryable al resto de mi aplicación? (5)
Por lo tanto, he leído todas las preguntas y respuestas aquí en SO sobre el tema de si exponer o no IQueryable al resto de su proyecto o no (consulte here y here ), y finalmente he decidido que no quiero para exponer IQueryable a cualquier cosa menos mi modelo. Debido a que IQueryable está vinculado a ciertas implementaciones de persistencia, no me gusta la idea de encerrarme en esto. Del mismo modo, no estoy seguro de lo bien que me siento con respecto a las clases en la cadena de llamadas que modifican la consulta real que no están en el repositorio.
Entonces, ¿alguien tiene alguna sugerencia sobre cómo escribir un Repositorio limpio y conciso sin hacer esto? Un problema que veo, es que mi Repositorio explotará a partir de una tonelada de métodos para varias cosas que necesito para filtrar mi consulta.
Tener un montón de:
IEnumerable GetProductsSinceDate(DateTime date);
IEnumberable GetProductsByName(string name);
IEnumberable GetProductsByID(int ID);
Si permitiera que IQueryable se transfiriera, podría tener fácilmente un repositorio genérico que parecía:
public interface IRepository<T> where T : class
{
T GetById(int id);
IQueryable<T> GetAll();
void InsertOnSubmit(T entity);
void DeleteOnSubmit(T entity);
void SubmitChanges();
}
Sin embargo, si no está usando IQueryable, los métodos como GetAll () no son realmente prácticos ya que la evaluación perezosa no se llevará a cabo en la línea. No quiero devolver 10,000 registros solo para usar 10 de ellos más tarde.
¿Cuál es la respuesta aquí? En MVC Storefront de Conery , creó otra capa llamada capa "Servicio" que recibió los resultados de IQueryable del repositorio y fue responsable de aplicar varios filtros.
¿Es esto lo que debería hacer, o algo similar? ¿Mi repositorio devuelve IQueryable pero restringe el acceso ocultándolo detrás de un grupo de clases de filtro como GetProductByName, que devolverá un tipo concreto como IList o IEnumerable?
Después de haber luchado para encontrar una solución viable para este problema, hay una solución que parece ser una buena solución en el artículo Implementación del Repositorio y la Unidad de trabajo en una aplicación ASP.NET MVC (9 de 10) .
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { '','' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
El artículo no habla sobre este problema exacto, pero sí sobre métodos genéricos y reutilizables de repositorio.
Hasta ahora, esto es todo lo que he podido encontrar como una solución.
El método de Rob realmente no resuelve su problema principal, y eso no significa escribir métodos individuales para cada tipo de consulta que le gustaría realizar, y desafortunadamente, si no está usando IQueryable, eso es lo que queda.
Seguro que los métodos pueden estar en la capa de "servicio", pero todavía significa tener que escribir "GetProductsByName, GetProductsByDate" ...
El otro método es algo como:
GetProducts(QueryObject);
Esto podría darle algún beneficio sobre el uso de IQueryable, ya que puede restringir lo que se devuelve.
Exponer un IQueryable
es una solución muy viable y esta es la forma en que la mayoría de las implementaciones de Repository están haciendo ahora mismo. (Incluyendo también SharpArchitecture y FubuMVC contrib.)
Aquí es donde te equivocas:
Sin embargo, si no está usando IQueryable, los métodos como GetAll () no son realmente prácticos ya que la evaluación perezosa no se llevará a cabo en la línea. No quiero devolver 10,000 registros solo para usar 10 de ellos más tarde.
Esto no es realmente cierto. Su ejemplo es correcto y debe cambiar el nombre de GetAll () a un nombre más informativo.
NO devuelve todos los artículos si lo llama. Para eso está IQueryable. El concepto se denomina "carga diferida", ya que solo carga los datos (y realiza solicitudes de base de datos) cuando enumera IQueryable
.
Entonces, digamos que tengo un método como este:
IQueryable<T> Retrieve() { ... }
Entonces, puedo llamarlo así:
Repository.Retrieve<Customer>().Single(c => c.ID == myID);
Esto SOLO recupera una fila de la base de datos.
Y esto:
Repository.Retrieve<Customer>().Where(c => c.FirstName == "Joe").OrderBy(c => c.LastName);
Esto también genera una consulta correspondiente y solo se ejecuta cuando la enumeras. (Genera un árbol de expresiones a partir de la consulta, y luego el proveedor de la consulta debería traducir eso en una consulta apropiada contra la fuente de datos).
Puedes leer más sobre esto en este artículo de MSDN .
Terminé creando dos conjuntos de métodos, uno que devuelve IEnumerable (en su caso, IQueryable) y otro que devuelve la Colección (extraiga el contenido antes de enviarlo del repositorio).
Esto me permite realizar tanto consultas ad hoc en Servicios fuera del repositorio como usar métodos del Repositorio que devuelven directamente las Colecciones resistentes a los efectos secundarios. En otras palabras, la unión de dos entidades de repositorio juntas da como resultado una consulta de selección, en lugar de una consulta de selección para cada entidad encontrada.
Me imagino que podría establecer su nivel de protección para evitar que sucedan cosas realmente malas.
hmm ... Resolví esto de muchas maneras dependiendo del tipo de ORM que utilizo.
La idea principal es tener una clase base de repositorio y un método de consulta que tome tantos parámetros que indiquen todas las opciones posibles where / orderby / expand | include / paging / etc.
Aquí hay una muestra rápida y sucia usando LINQ to NHibernate (por supuesto, todo el repositorio debe ser un detalle de implementación):
public class RepositoryBase
{
private ISession Session;
public RepositoryBase()
{
Session = SessionPlaceHolder.Session;
}
public TEntity[] GetPaged<TEntity>(IEnumerable<Expression<Func<TEntity, bool>>> filters,
IEnumerable<Expression<Func<TEntity, object>>> relatedObjects,
IEnumerable<Expression<Func<TEntity, object>>> orderCriterias,
IEnumerable<Expression<Func<TEntity, object>>> descOrderCriterias,
int pageNumber, int pageSize, out int totalPages)
{
INHibernateQueryable<TEntity> nhQuery = Session.Linq<TEntity>();
if (relatedObjects != null)
foreach (var relatedObject in relatedObjects)
{
if (relatedObject == null) continue;
nhQuery = nhQuery.Expand(relatedObject);
}
IQueryable<TEntity> 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();
}
}
Normalmente querrá agregar muchas sobrecargas de encadenamiento como accesos directos cuando no necesite la paginación, por ejemplo, etc.
Aquí hay otro sucio. Lo siento, no estoy seguro de poder exponer los últimos. Esos fueron borradores y están bien para mostrar:
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<T>(Expression<Func<T, bool>> filter, out T result)
{
result = Find(filter, null).FirstOrDefault();
return !Equals(result, default(T));
}
public T FindOne<T>(Expression<Func<T, bool>> filter)
{
T result;
if (TryFindOne(filter, out result))
return result;
return default(T);
}
public IList<T> Find<T>() where T : class, IEntityWithKey
{
int count;
return new List<T>(Find<T>(null, null, 0, 0, out count));
}
public IList<T> Find<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort)
{
int count;
return new List<T>(Find(filter, sort, 0, 0, out count));
}
public IEnumerable<T> Find<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort, int pageSize,
int pageNumber, out int count)
{
return ExecuteQuery(filter, sort, pageSize, pageNumber, out count) ?? new T[] {};
}
public bool Save<T>(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<T>(Expression<Func<T, bool>> filter)
{
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
int numberOfObjectsFound = 0;
foreach (T entity in context.CreateQuery<T>(GetEntitySetName(typeof (T))).Where(filter))
{
context.DeleteObject(entity);
++numberOfObjectsFound;
}
return context.SaveChanges() >= numberOfObjectsFound;
}
#endregion
protected IEnumerable<T> ExecuteQuery<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort,
int pageSize, int pageNumber,
out int count)
{
IEnumerable<T> result;
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
ObjectQuery<T> originalQuery = CreateQuery<T>(context);
IQueryable<T> 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<ObjectContext, IEnumerable> EntitiesFound;
protected void OnEntitiesFound<T>(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<IEnumerable> ItemsFound;
protected IEnumerable<T> ExecuteQuery<T>(ObjectContext context, IQueryable<T> query)
{
IEnumerable<T> result = null;
if (query is ObjectQuery)
{
var objectQuery = (ObjectQuery<T>) query;
objectQuery.EnablePlanCaching = false;
objectQuery.MergeOption = MergeOption.PreserveChanges;
result = new List<T>(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<T> CreateQuery<T>(ObjectContext context)
{
ObjectQuery<T> query = context.CreateQuery<T>(GetEntitySetName(typeof (T)));
query = this.AggregateEntities(query);
return query;
}
protected virtual ObjectQuery<T> AggregateEntities<T>(ObjectQuery<T> 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<T> Include<T>(this ObjectQuery<T> query,
Expression<Func<T, object>> 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);
}
}
}
En MVC Storefront de Conery, creó otra capa llamada capa "Servicio" que recibió los resultados de IQueryable del repositorio y fue responsable de aplicar varios filtros.
En todos los casos, nadie debe interactuar con el repositorio directamente, excepto la capa de servicios.
Lo más flexible es dejar que los Servicios interactúen con el Repositorio de la forma que deseen, igual que en el código anterior (pero a través de un solo punto, como en el ejemplo también, para escribir código DRY y encontrar un lugar para la optimización).
Sin embargo, la forma más correcta en términos de patrones DDD comunes es usar el patrón de "Especificación", donde encapsula todos sus filtros, etc. en Variables (Miembros de la Clase, en LINQ típicamente de los tipos de delegado). LINQ puede sacar un gran beneficio de optimización al combinarlo con "Consultas compiladas". Si busca en Google {Patrón de especificación} y {Consultas compiladas LINQ} se acercará más a lo que quiero decir aquí.