Pregunta

Entonces, he leído todas las preguntas y respuestas aquí sobre SO con respecto al tema de si exponer o no IQueryable al resto de su proyecto o no (ver aquí , y aquí ), y finalmente decidí que no quiero exponer IQueryable a otra cosa que no sea 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 acerca de las clases más abajo en la cadena de llamadas modificando la consulta real que no está 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á 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 se pasara IQueryable, fácilmente podría tener un repositorio genérico similar al siguiente:

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á utilizando IQueryable, los métodos como GetAll () no son realmente prácticos, ya que no se realizará una evaluación diferida en el futuro. 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 " Servicio " capa que recibió resultados IQueryable del repositorio y fue responsable de aplicar varios filtros.

¿Es esto lo que debo hacer, o algo similar? ¿Mi repositorio devuelve IQueryable pero restringe el acceso a él ocultándolo detrás de un grupo de clases de filtro como GetProductByName, que devolverá un tipo concreto como IList o IEnumerable?

¿Fue útil?

Solución

Exponer un IQueryable es una solución muy viable y así es como lo están haciendo la mayoría de las implementaciones del Repositorio en este momento. (Incluyendo SharpArchitecture y FubuMVC contrib, también.)

Aquí es donde te equivocas:

  

Sin embargo, si no estás usando   IQueryable luego métodos como GetAll ()   no son realmente prácticos ya que vagos   la evaluación no se llevará a cabo   la línea. No quiero volver   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 elementos si lo llama. Para eso es 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 enumera. (Genera un árbol de expresión a partir de la consulta, y luego el proveedor de la consulta debe traducirlo en una consulta apropiada contra la fuente de datos).

Puede leer más al respecto en este artículo de MSDN .

Otros consejos

El método de Rob realmente no resuelve su problema central, y eso no es querer escribir métodos individuales para cada tipo de consulta que le gustaría ejecutar, y desafortunadamente si no está usando IQueryable, eso es lo que está haciendo. dejado con.

Seguro que los métodos pueden estar en el "servicio" capa, pero aún significa tener que escribir '' GetProductsByName, GetProductsByDate '' ...

El otro método es algo así como:

GetProducts(QueryObject);

Esto podría brindarle algún beneficio sobre el uso de IQueryable, ya que puede restringir lo que se devuelve.

hmm .. Resolví esto de muchas maneras dependiendo del tipo de ORM que use.
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 donde / 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 paginación, por ejemplo, etc.

Aquí hay otro sucio. Lo siento, no estoy seguro si puedo 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 Conery's MVC Storefront creó   otra capa llamada el "Servicio"   capa que recibió IQueryable   resultados del repositorio y fue   responsable de aplicar varios   filtros.

En todos los casos, nadie debería interactuar directamente con el repositorio excepto la capa de servicios.

Lo más flexible es permitir 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 por ejemplo también, 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 la " Especificación " patrón, donde encapsula todos sus filtros, etc.en Variables (Miembros de la clase, en LINQ típicamente de tipos delegados). LINQ puede obtener un gran beneficio de optimización de esto cuando lo combina con " consultas compiladas " ;. Si buscas en Google el {Patrón de especificación} y las {Consultas compiladas LINQ}, te acercarás a lo que quiero decir aquí.

Terminé creando dos conjuntos de métodos, uno que devuelve IEnumerable (en su caso, IQueryable) y otro que devuelve Colección (extraiga el contenido antes de enviarlo fuera del repositorio).

Esto me permite crear consultas ad hoc en Servicios fuera del repositorio, así como usar métodos de Repositorio que devuelven directamente Colecciones resistentes a los efectos secundarios. En otras palabras, unir 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.

Habiendo luchado por encontrar una solución viable a este problema, hay una buena solución en Implementando los patrones del repositorio y la unidad de trabajo en un Artículo ASP.NET MVC Application (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í habla sobre métodos de repositorio genéricos y reutilizables.

Hasta ahora, esto es todo lo que he podido encontrar como solución.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top