33 votes

Méthodes de dépôt vs. extension de IQueryable

Je dispose de référentiels (par exemple ContactRepository, UserRepository, etc.) qui encapsulent l'accès aux données du modèle de domaine.

Quand je regardais recherche de données par exemple

  • trouver un contact dont le prénom commence par XYZ
  • un contact dont l'anniversaire est postérieur à 1960

    (etc),

J'ai commencé à mettre en œuvre des méthodes de référentiel telles que FirstNameStartsWith(string prefix) y Plus jeune que l'année de naissance(int année) en suivant les nombreux exemples existants.

Puis j'ai rencontré un problème : que faire si je dois combiner plusieurs recherches ? Chacune de mes méthodes de recherche de référentiel, comme ci-dessus, ne renvoie qu'un ensemble fini d'objets réels du domaine. À la recherche d'une meilleure méthode, j'ai commencé à écrire méthodes d'extension sur IQueryable<T>, par exemple this :

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

Maintenant, je peux faire des choses comme

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

Cependant, je me suis retrouvé à écrire des méthodes d'extension (et à inventer des classes farfelues telles que ContactsQueryableExtensions partout, et je perds le "beau regroupement" en ayant tout dans le dépôt approprié.

Est-ce vraiment la bonne façon de procéder, ou existe-t-il un meilleur moyen d'atteindre le même objectif ?

12voto

Chad Ruppert Points 3073

J'ai beaucoup réfléchi à ce sujet ces derniers temps, après avoir commencé mon travail actuel. Je suis habitué aux référentiels, ils vont jusqu'au bout de la démarche IQueryable en utilisant seulement des référentiels de base comme vous le suggérez.

J'ai le sentiment que le modèle repo est solide et qu'il décrit de manière semi-efficace la façon dont vous voulez travailler avec les données dans le domaine d'application. Cependant, le problème que vous décrivez existe bel et bien. Cela devient vite désordonné, au-delà d'une simple application.

Y a-t-il, peut-être, des moyens de repenser la raison pour laquelle vous demandez les données de tant de façons ? Si ce n'est pas le cas, je pense vraiment qu'une approche hybride est la meilleure solution. Créez des méthodes de dépôt pour les éléments que vous réutilisez. Les choses pour lesquelles cela a un sens. DRY et tout ça. Mais ces éléments uniques ? Pourquoi ne pas profiter de IQueryable et des choses sexy que vous pouvez faire avec ? C'est idiot, comme vous l'avez dit, de créer une méthode pour cela, mais cela ne veut pas dire que vous n'avez pas besoin des données. DRY ne s'applique pas vraiment ici, n'est-ce pas ?

Il faut de la discipline pour bien faire cela, mais je pense vraiment que c'est une voie appropriée.

7voto

Pure.Krome Points 28473

@Alex - Je sais que c'est une vieille question, mais ce que je ferais serait de laisser le référentiel faire des choses vraiment simples seulement. Cela signifie qu'il faut obtenir tous les enregistrements d'une table ou d'une vue.

Ensuite, dans la couche SERVICES (vous utilisez une solution à n niveaux, n'est-ce pas ? :) ), je traiterais toutes les requêtes "spéciales".

Ok, c'est l'heure de l'exemple.

Couche de dépôt

ContactRepository.cs

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

Sympathique et simple. SqlContext est l'instance de votre EF Context qui a un Entity sur elle appelé Contacts qui est fondamentalement votre classe de contacts sql.

Cela signifie que cette méthode est en train d'agir : SELECT * FROM CONTACTS ... mais il ne frappe pas la base de données avec cette requête ... c'est seulement une requête pour le moment.

Ok couche suivante KICK ... on y va ( Inception quelqu'un ?)

Couche services

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

C'est fait.

Récapitulons. Tout d'abord, nous commençons par un simple ' Tout obtenir à partir des contacts Requête. Maintenant, si un nom est fourni, ajoutons un filtre pour filtrer tous les contacts par nom. Ensuite, si nous avons une année fournie, alors nous filtrons les anniversaires par année. Etc. Enfin, nous interrogeons la base de données (avec cette requête modifiée) et voyons quels résultats nous obtenons.

NOTES:-

  • J'ai omis toute injection de dépendance pour des raisons de simplicité. C'est plus que fortement recommandé.
  • Tout ceci est du code pseduo. Non testé (contre un compilateur) mais vous avez l'idée ....

Points à retenir

  • La couche Services s'occupe de tout ce qui est intelligent. C'est là que vous décidez des données dont vous avez besoin.
  • Le référentiel est un simple SELECT * FROM TABLE ou un simple INSERT/UPDATE dans TABLE.

Bonne chance :)

5voto

robjb Points 3049

Je me rends compte que c'est vieux, mais j'ai été confronté à ce même problème récemment, et je suis arrivé à la même conclusion que Chad : avec un peu de discipline, un hybride de méthodes d'extension et de méthodes de dépôt semble fonctionner au mieux.

Quelques règles générales que j'ai suivies dans mon application (Entity Framework) :

Demandes de renseignements sur les commandes

Si la méthode n'est utilisée que pour la commande, je préfère écrire des méthodes d'extension qui opèrent sur IQueryable<T> o IOrderedQueryable<T> (pour tirer parti du fournisseur sous-jacent). par exemple

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

Maintenant je peux utiliser ThenByStudentName() selon les besoins dans ma classe de référentiel.

Requêtes retournant des instances uniques

Si la méthode implique l'interrogation par des paramètres primitifs, elle nécessite généralement un fichier ObjectContext et ne peuvent pas être fabriqués facilement static . Ces méthodes, je les laisse sur mon référentiel, par exemple

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

Toutefois, si la méthode consiste plutôt à interroger une EntityObject en utilisant son propriétés de navigation il peut généralement être fait static assez facilement, et mis en œuvre comme une méthode d'extension. par exemple

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

Maintenant, je peux facilement écrire someStudent.GetLatestRegistration() sans avoir besoin d'une instance de référentiel dans la portée actuelle.

Requêtes retournant des collections

Si la méthode renvoie un IEnumerable , ICollection o IList alors j'aime le faire static si possible, et le laisser sur le référentiel même s'il utilise des propriétés de navigation. par exemple

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

C'est parce que mon GetAll() sont déjà présentes dans le référentiel, et cela permet d'éviter un fouillis de méthodes d'extension.

Une autre raison de ne pas implémenter ces "récupérateurs de collection" en tant que méthodes d'extension est qu'ils nécessiteraient une dénomination plus verbeuse pour être significatifs, puisque le type de retour n'est pas implicite. Par exemple, le dernier exemple deviendrait GetTermRegistrationsByTerm(this Term term) .

J'espère que cela vous aidera !

0voto

one.beat.consumer Points 5612

Six ans plus tard, je suis certain que @Alex a résolu son problème, mais après avoir lu la réponse acceptée, je voulais ajouter mon grain de sel.

L'objectif général de l'extension IQueryable des collections dans un dépôt pour offrir une certaine souplesse et permettre à ses consommateurs de personnaliser la recherche de données. Ce qu'Alex a déjà fait est du bon travail.

Le rôle principal d'un couche de service est d'adhérer à la séparation des préoccupations principe et adresse logique de commande associée à la fonction commerciale.

Dans les applications du monde réel, logique de requête ne nécessite souvent aucune extension au-delà des mécanismes de récupération fournis par le référentiel lui-même (ex. altérations de valeurs, conversions de types).

Considérez les deux scénarios suivants :

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

Les deux méthodes sont de simples extensions de requêtes, mais aussi subtiles soient-elles, elles ont des rôles différents. La première est un simple filtre, tandis que la seconde implique un besoin commercial (ex. maintenance ou facturation). Dans une application simple, on pourrait les mettre en œuvre toutes les deux dans un référentiel. Dans un système plus idéaliste UsedThisYear est mieux adapté à la couche service (et peut même être implémenté comme une méthode d'instance normale) où il peut également mieux faciliter l'utilisation de la méthode CQRS stratégie de séparation commandes y demandes de renseignements .

Les éléments clés à prendre en considération sont (a) l'objectif principal de votre référentiel et (b) dans quelle mesure vous aimez adhérer à CQRS y DDD les philosophies.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X