Domanda

Quindi, ho letto tutte le domande e risposte qui su SO riguardo all'argomento se esporre o meno IQueryable al resto del tuo progetto (vedi qui e qui ) e alla fine ho deciso che non volevo esporre IQueryable a tutto tranne che al mio modello. Poiché IQueryable è legato a determinate implementazioni di persistenza, non mi piace l'idea di bloccarmi in questo. Allo stesso modo, non sono sicuro di come mi sento bene riguardo alle classi più in basso nella catena di chiamate modificando la query effettiva che non si trova nel repository.

Quindi, qualcuno ha qualche suggerimento su come scrivere un repository pulito e conciso senza farlo? Un problema che vedo è che il mio repository esploderà da un sacco di metodi per varie cose di cui ho bisogno di filtrare la mia query.

Avere un sacco di:

IEnumerable GetProductsSinceDate(DateTime date);  
IEnumberable GetProductsByName(string name);  
IEnumberable GetProductsByID(int ID);

Se consentissi il passaggio di IQueryable potrei facilmente avere un repository generico che assomigliava 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();
}

Tuttavia, se non si utilizza IQueryable, metodi come GetAll () non sono molto pratici poiché la valutazione lazy non avrà luogo in futuro. Non voglio restituire 10.000 dischi solo per usarne 10 in seguito.

Qual è la risposta qui? In Storefront MVC di Conery ha creato un altro livello chiamato " Service " livello che ha ricevuto i risultati IQueryable dal repository ed era responsabile dell'applicazione di vari filtri.

È questo che dovrei fare o qualcosa di simile? Il mio repository restituisce IQueryable ma limita l'accesso ad esso nascondendolo dietro un gruppo di classi di filtri come GetProductByName, che restituirà un tipo concreto come IList o IEnumerable?

È stato utile?

Soluzione

Esporre un IQueryable è una soluzione molto praticabile e questo è il modo in cui la maggior parte delle implementazioni del repository là fuori stanno facendo proprio ora. (Incluso anche SharpArchitecture e FubuMVC contrib.)

Qui è dove ti sbagli:

  

Tuttavia, se non lo stai utilizzando   IQueryable quindi metodi come GetAll ()   non sono davvero pratici da quando sono pigri   la valutazione non avrà luogo   la linea. Non voglio tornare   10.000 record solo per usarne 10   più tardi.

Questo non è proprio vero. Il tuo esempio è corretto e dovresti rinominare GetAll () con un nome più informativo.

NON restituisce tutti gli articoli se lo chiami. Ecco a cosa serve IQueryable. Il concetto si chiama "caricamento posticipato", poiché carica i dati (ed effettua richieste di database) solo quando si enumera IQueryable .

Quindi, diciamo che ho un metodo come questo:

IQueryable<T> Retrieve() { ... }

Quindi, posso chiamarlo così:

Repository.Retrieve<Customer>().Single(c => c.ID == myID);

Questo recupera SOLO una riga dal database.

E questo:

Repository.Retrieve<Customer>().Where(c => c.FirstName == "Joe").OrderBy(c => c.LastName);

Ciò genera anche una query corrispondente e viene eseguita solo quando la si enumera. (Genera un albero delle espressioni dalla query, quindi il provider di query dovrebbe tradurlo in una query appropriata rispetto all'origine dati.)

Puoi leggere di più al riguardo in questo articolo di MSDN .

Altri suggerimenti

Il metodo di Rob non risolve davvero il tuo problema principale, e questo non vuole scrivere singoli metodi per ogni tipo di query che desideri eseguire, e sfortunatamente se non stai usando IQueryable, questo è quello che sei lasciato con.

Sicuramente i metodi potrebbero essere nel "servizio" livello, ma significa comunque dover scrivere " GetProductsByName, GetProductsByDate " ...

L'altro metodo è simile a:

GetProducts(QueryObject);

Questo potrebbe darti qualche vantaggio sull'uso di IQueryable in quanto puoi limitare ciò che viene restituito.

hmm .. L'ho risolto in molti modi a seconda del tipo di ORM che utilizzo.
L'idea principale è quella di avere una classe base di repository e un metodo di query che accetta così tanti parametri che indicano tutte le possibili opzioni dove / orderby / expand | include / paging / etc.

Ecco un esempio veloce e sporco che usa LINQ per NHibernate (ovviamente l'intero repository dovrebbe essere dettagli di implementazione):

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 ti consigliamo di aggiungere molti sovraccarichi concatenati come scorciatoie quando non hai bisogno di cercapersone, ad esempio, ecc.

Eccone un altro sporco. Scusa, non sono sicuro di poter esporre quelli finali. Quelle erano bozze e sono OK per mostrare:

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);
        }
    }
}
  

In Conery's MVC Storefront ha creato   un altro livello chiamato "Servizio"   strato che ha ricevuto IQueryable   risultati dal repository ed era   responsabile per l'applicazione di vari   filtri.

In tutti i casi nessuno dovrebbe interagire direttamente con il repository tranne il livello servizi.

La cosa più flessibile è lasciare che i Servizi interagiscano con il Repository come vogliono, come nel codice sopra (ma attraverso un singolo punto, come ad esempio anche per scrivere codice DRY e trovare un posto per l'ottimizzazione).
Tuttavia, il modo più corretto in termini di schemi DDD comuni è utilizzare la "Specifica" modello, in cui si incapsulano tutti i filtri, ecc. in Variabili (membri della classe, in LINQ in genere di tipi delegati). LINQ può trarre grandi vantaggi dall'ottimizzazione quando lo combini con " Query compilate " ;. Se vai su {Specification Pattern} e {LINQ Compiled Queries} ti avvicinerai a ciò che intendo qui.

Ho finito per creare due set di metodi, uno che restituisce IEnumerable (nel tuo caso IQueryable) e uno che restituisce Collection (estrae il contenuto prima di inviarlo dal repository.)

Ciò mi consente di creare query ad hoc in Servizi al di fuori del repository, nonché di utilizzare metodi di repository che restituiscono direttamente raccolte resistenti agli effetti collaterali. In altre parole, l'unione di due entità di repository genera una query di selezione anziché una query di selezione per ciascuna entità trovata.

Immagino che potresti impostare il tuo livello di protezione per evitare che accadano cose veramente brutte.

Avendo faticato a trovare una soluzione praticabile a questo problema, c'è quella che sembra essere una buona soluzione in Implementazione del repository e Unit of Work Patterns in un Applicazione ASP.NET MVC (9 di 10) articolo.

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();
        }
    }

L'articolo non parla di questo problema esatto ma parla di metodi di repository generici e riutilizzabili.

Finora è tutto ciò che sono riuscito a trovare come soluzione.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top