Domanda

Ho repository (ad esempio ContactRepository, UserRepository e così via) che incapsulano l'accesso ai dati al modello di dominio.

Quando stavo guardando cercando dati , ad es.

  • trovare un contatto il cui nome inizia con XYZ
  • un contatto il cui compleanno è trascorso 1960

    (ecc),

Ho iniziato a implementare metodi di repository come FirstNameStartsWith (prefisso stringa) e YoungerThanBirthYear (int year) , sostanzialmente seguendo i numerosi esempi disponibili.

Quindi ho riscontrato un problema: cosa succede se devo combinare più ricerche? Ciascuno dei miei metodi di ricerca nel repository, come sopra, restituisce solo un set finito di oggetti di dominio effettivi. Alla ricerca di un modo migliore, ho iniziato a scrivere metodi di estensione su IQueryable & Lt; T & Gt ;, ad es. in questo modo:

public static IQueryable<Contact> FirstNameStartsWith(
               this IQueryable<Contact> contacts, String prefix)
{
    return contacts.Where(
        contact => contact.FirstName.StartsWith(prefix));
}        

Ora posso fare cose come

ContactRepository.GetAll().FirstNameStartsWith("tex").YoungerThanBirthYear(1960);

Tuttavia, mi sono ritrovato a scrivere metodi di estensione (e inventare classi pazze come ContactsQueryableExtensions dappertutto, e perdo la " bel raggruppamento " avendo tutto nel repository appropriato.

È davvero questo il modo di farlo o esiste un modo migliore per raggiungere lo stesso obiettivo?

È stato utile?

Soluzione

@Alex - So che questa è una vecchia domanda, ma cosa farei sarebbe lasciare che il Repository faccia cose davvero semplici . Ciò significa che ottieni tutti i record per una tabella o una vista.

Quindi, nel livello SERVIZI (stai usando una soluzione a più livelli, giusto? :)) gestirò tutte le query "speciali" lì.

Ok, ora di esempio.

Livello repository

ContactRepository.cs

public IQueryable<Contact> GetContacts()
{
    return (from q in SqlContext.Contacts
            select q).AsQueryable();
}

Bello e semplice. SqlContext è l'istanza del tuo EF Context .. che contiene un Entity chiamato Contacts .. che è fondamentalmente la tua classe di contatti sql.

Ciò significa che in pratica quel metodo sta facendo: SELECT * FROM CONTACTS ... ma non sta colpendo il database con quella query .. è solo una query in questo momento.

Ok .. prossimo livello .. KICK ... su andiamo ( Inception chiunque?)

Livello servizi

ContactService.cs

public  ICollection<Contact> FindContacts(string name)
{
    return FindContacts(name, null)
}

public ICollection<Contact> FindContacts(string name, int? year)
{
   IQueryable<Contact> query = _contactRepository.GetContacts();

   if (!string.IsNullOrEmpty(name))
   {
       query = from q in query
               where q.FirstName.StartsWith(name)
               select q;
   }

   if (int.HasValue)
   {
       query = from q in query
               where q.Birthday.Year <= year.Value
               select q);
    }

    return (from q in query
            select q).ToList();
}

Fatto.

Quindi ricapitoliamo. Innanzitutto, iniziamo con una semplice query " Ottieni tutto dai contatti ". Ora, se abbiamo un nome fornito, consente di aggiungere un filtro per filtrare tutti i contatti per nome. Successivamente, se viene fornito un anno, filtriamo il compleanno per Anno. Ecc. Infine, colpiamo il DB (con questa query modificata) e vediamo quali risultati otteniamo.

NOTE: -

  • Ho omesso qualsiasi iniezione di dipendenza per semplicità. È più che altamente raccomandato.
  • Questo è tutto codice pseduo. Non testato (contro un compilatore) ma ottieni l'idea ....

Punti da asporto

  • Il livello Servizi gestisce tutte le risorse intelligenti. È qui che decidi quali dati sono necessari.
  • Il repository è un semplice SELECT * FROM TABLE o un semplice INSERT / UPDATE in TABLE.

Buona fortuna :)

Altri suggerimenti

Ci ho pensato molto ultimamente, dopo aver iniziato il mio lavoro attuale. Sono abituato ai repository, seguono il percorso IQueryable completo usando solo repository di ossa nude come suggerisci tu.

Sento che il modello di pronti contro termine è valido e fa un lavoro semi-efficace nel descrivere come si desidera lavorare con i dati nel dominio dell'applicazione. Tuttavia, si verifica sicuramente il problema che stai descrivendo. Diventa disordinato, veloce, oltre una semplice applicazione.

Esistono forse modi per ripensare il motivo per cui stai chiedendo i dati in così tanti modi? Altrimenti, penso davvero che un approccio ibrido sia il modo migliore di procedere. Crea metodi repo per le cose che riutilizzi. Roba per cui in realtà ha senso. SECCO e tutto il resto. Ma quei pezzi unici? Perché non approfittare di IQueryable e delle cose sexy che puoi fare con esso? È stupido, come hai detto, creare un metodo per questo, ma non significa che non hai bisogno dei dati. DRY non si applica davvero, vero?

Ci vorrebbe disciplina per farlo bene, ma penso davvero che sia un percorso appropriato.

Mi rendo conto che questo è vecchio, ma ultimamente ho avuto a che fare con lo stesso problema e sono giunto alla stessa conclusione del Ciad: con un po 'di disciplina, un ibrido di metodi di estensione e metodi di deposito sembra funzionare meglio.

Alcune regole generali che ho seguito nella mia applicazione (Entity Framework):

Richiesta di ordini

Se il metodo viene utilizzato solo per l'ordinazione, preferisco scrivere metodi di estensione che funzionano su IQueryable<T> o IOrderedQueryable<T> (per sfruttare il fornitore sottostante.) e.g.

public static IOrderedQueryable<TermRegistration> ThenByStudentName(
    this IOrderedQueryable<TermRegistration> query)
{
    return query
        .ThenBy(reg => reg.Student.FamilyName)
        .ThenBy(reg => reg.Student.GivenName);
}

Ora posso usare ThenByStudentName() secondo necessità all'interno della mia classe di repository.

Query che restituiscono singole istanze

Se il metodo prevede l'interrogazione mediante parametri primitivi, di solito richiede un ObjectContext e non può essere facilmente creato static. Questi metodi che lascio sul mio repository, e.g.

public Student GetById(int id)
{
    // Calls context.ObjectSet<T>().SingleOrDefault(predicate) 
    // on my generic EntityRepository<T> class 
    return SingleOrDefault(student => student.Active && student.Id == id);
}

Tuttavia, se il metodo prevede invece la query di un EntityObject utilizzando le sue proprietà di navigazione , di solito può essere creato someStudent.GetLatestRegistration() abbastanza facilmente e implementato come metodo di estensione. per es.

public static TermRegistration GetLatestRegistration(this Student student)
{
    return student.TermRegistrations.AsQueryable()
        .OrderByTerm()
        .FirstOrDefault();
}

Ora posso scrivere comodamente IEnumerable senza bisogno di un'istanza di repository nell'ambito corrente.

Query che restituiscono raccolte

Se il metodo restituisce alcuni ICollection, IList o GetAll(), allora mi piace renderlo GetTermRegistrationsByTerm(this Term term) se possibile, e lasciarlo nel repository anche se utilizza le proprietà di navigazione. eg

public static IList<TermRegistration> GetByTerm(Term term, bool ordered)
{
    var termReg = term.TermRegistrations;
    return (ordered)
        ? termReg.AsQueryable().OrderByStudentName().ToList()
        : termReg.ToList();
}

Questo perché i miei metodi <=> vivono già nel repository e aiuta a evitare un disordine ingombro di metodi di estensione.

Un altro motivo per non implementare questi " collezionisti " come metodi di estensione è che richiederebbero nomi più dettagliati per essere significativi, poiché il tipo restituito non è implicito. Ad esempio, l'ultimo esempio diventerebbe <=>.

Spero che questo aiuti!

Sei anni dopo, sono certo che @Alex abbia risolto il suo problema, ma dopo aver letto la risposta accettata volevo aggiungere i miei due centesimi.

Lo scopo generale di estendere IQueryable raccolte in un repository per fornire flessibilità e consentire ai propri consumatori di personalizzare il recupero dei dati. Ciò che Alex ha già fatto è un buon lavoro.

Il ruolo principale di un livello di servizio è aderire al principio di separazione delle preoccupazioni e indirizzare la logica di comando associata alla funzione aziendale.

Nelle applicazioni del mondo reale, la logica delle query spesso non ha bisogno di estensioni oltre alla meccanica di recupero fornita dal repository stesso (es. alterazioni di valore, conversioni di tipi).

Considera i due seguenti scenari:

IQueryable<Vehicle> Vehicles { get; }

// raw data
public static IQueryable<Vehicle> OwnedBy(this IQueryable<Vehicle> query, int ownerId)
{
    return query.Where(v => v.OwnerId == ownerId);
}

// business purpose
public static IQueryable<Vehicle> UsedThisYear(this IQueryable<Vehicle> query)
{
    return query.Where(v => v.LastUsed.Year == DateTime.Now.Year);
}

Entrambi i metodi sono semplici estensioni di query, ma per quanto sottili abbiano ruoli diversi. Il primo è un semplice filtro, mentre il secondo implica esigenze aziendali (es. Manutenzione o fatturazione). In una semplice applicazione è possibile implementarli entrambi in un repository. In un sistema più idealistico UsedThisYear è più adatto al livello di servizio (e può anche essere implementato come un normale metodo di istanza) dove può anche facilitare meglio la strategia CQRS di separazione dei comandi e query .

Le considerazioni chiave sono (a) lo scopo principale del tuo repository e (b) quanto ti piace aderire alle filosofie CQRS e DDD .

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