Методы репозитория против расширения IQueryable - PullRequest
33 голосов
/ 13 сентября 2009

У меня есть репозитории (например, ContactRepository, UserRepository и т. Д.), Которые инкапсулируют доступ к данным в модель предметной области.

Когда я смотрел на в поисках данных , например

  • поиск контакта, имя которого начинается с XYZ
  • контакт, чей день рождения после 1960

    (и т.д.),

Я начал реализовывать методы репозитория, такие как FirstNameStartsWith (префикс строки) и YoungerThanBirthYear (int year) , в основном следуя многим примерам.

Тогда я столкнулся с проблемой - что если мне нужно объединить несколько поисков? Каждый из моих методов поиска в репозитории, таких как выше, возвращает только конечный набор реальных доменных объектов. В поисках лучшего способа я начал писать методы расширения для IQueryable , например. это:

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

Теперь я могу делать такие вещи, как

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

Однако я обнаружил, что пишу методы расширения (и изобретаю сумасшедшие классы, такие как ContactsQueryableExtensions , и теряю «красивую группировку», имея все в соответствующем репозитории.

Это действительно способ сделать это, или есть лучший способ достичь той же цели?

Ответы [ 4 ]

12 голосов
/ 13 сентября 2009

Я много думал об этом в последнее время, после того, как начал работать. Я привык к репозиториям, они идут по полному пути IQueryable, используя просто скелеты, как вы предлагаете.

Мне кажется, что шаблон репо является надежным и выполняет полуэффективную работу по описанию того, как вы хотите работать с данными в домене приложения. Однако проблема, которую вы описываете, определенно возникает. Это становится грязным, быстрым, за пределами простого приложения.

Возможно, есть ли способы переосмыслить, почему вы запрашиваете данные многими способами? Если нет, я действительно чувствую, что гибридный подход - лучший путь. Создайте методы репо для материала, который вы используете повторно. Материал, который на самом деле имеет смысл. СУХОЙ и все такое. Но эти разовые? Почему бы не воспользоваться IQueryable и сексуальными вещами, которые вы можете сделать с ним? Как вы сказали, глупо создавать метод для этого, но это не значит, что вам не нужны данные. СУХОЙ на самом деле не применяется там, не так ли?

Требуется дисциплина, чтобы сделать это хорошо, но я действительно думаю, что это правильный путь.

6 голосов
/ 02 августа 2010

@ Алекс - я знаю, что это старый вопрос, но я бы позволил хранилищу делать действительно простых вещей . Это значит, получить все записи для таблицы или представления.

Затем на уровне СЕРВИСОВ (вы используете n-уровневое решение, верно? :)) я бы обработал все «специальные» запросы там.

Хорошо, пример времени.

Слой репозитория

ContactRepository.cs

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

Красиво и просто. SqlContext - это экземпляр вашего EF Context .., на котором есть Entity с именем Contacts .., который в основном является вашим классом контактов sql.

Это означает, что этот метод в основном выполняет: SELECT * FROM CONTACTS ... но он не попадает в базу данных этим запросом ... это всего лишь запрос прямо сейчас.

Ладно .. следующий слой .. KICK ... наверх ( Начало кто-нибудь?)

Уровень услуг

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

Готово.

Итак, подведем итоги. Сначала мы начнем с простого запроса Получить все из контактов . Теперь, если у нас есть имя, давайте добавим фильтр для фильтрации всех контактов по имени. Далее, если у нас есть год, мы фильтруем день рождения по Году. И т.д. Наконец, мы затем нажимаем на БД (с этим измененным запросом) и видим, какие результаты мы получим.

ПРИМЕЧАНИЯ: -

  • Я упустил любую инъекцию зависимостей для простоты. Это более чем настоятельно рекомендуется.
  • Это весь псевдо-код. Не проверено (против компилятора), но вы поняли идею ...

Баллы на вынос

  • Слой Services обрабатывает все смарты. Именно здесь вы решаете, какие данные вам нужны.
  • Репозиторий представляет собой простой SELECT * FROM TABLE или простой INSERT / UPDATE в TABLE.

Удачи:)

5 голосов
/ 15 февраля 2012

Я понимаю, что это старая проблема, но в последнее время я занимаюсь этой же проблемой, и я пришел к тому же выводу, что и Чад: при небольшой дисциплине гибрид методов расширения и методов хранилища, кажется, работает лучше всего.

Некоторые общие правила, которым я следовал в своем приложении (Entity Framework):

Заказ запросов

Если метод используется только для упорядочивания, я предпочитаю писать методы расширения, которые работают на IQueryable<T> или IOrderedQueryable<T> (чтобы использовать базового провайдера.) Например,

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

Теперь я могу использовать ThenByStudentName() по мере необходимости в моем классе репозитория.

Запросы, возвращающие отдельные экземпляры

Если метод включает запросы по примитивным параметрам, он обычно требует ObjectContext и не может быть легко выполнен static. Эти методы я оставляю в своем хранилище, , например

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

Однако, если вместо этого метод включает запрос EntityObject с использованием его навигационных свойств , его обычно можно легко сделать static и реализовать как метод расширения. , например

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

Теперь я могу легко написать someStudent.GetLatestRegistration(), не нуждаясь в экземпляре репозитория в текущей области.

Запросы, возвращающие коллекции

Если метод возвращает некоторые значения IEnumerable, ICollection или IList, то я хотел бы сделать его static, если это возможно, и оставить его в хранилище , даже если он использует свойства навигации. например

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

Это потому, что мои GetAll() методы уже живут в репозитории, и это помогает избежать беспорядочной путаницы методов расширения.

Еще одна причина, по которой эти "методы получения коллекций" не используются в качестве методов расширения, заключается в том, что им потребуется более подробное именование, чтобы иметь смысл, поскольку возвращаемый тип не подразумевается. Например, последним примером станет GetTermRegistrationsByTerm(this Term term).

Надеюсь, это поможет!

0 голосов
/ 10 марта 2016

Шесть лет спустя, я уверен, @Alex решил его проблему, но после прочтения принятого ответа я хотел добавить свои два цента.

Общая цель расширения IQueryable коллекций в хранилище , чтобы обеспечить гибкость и дать возможность своим пользователям настраивать поиск данных. Алекс уже сделал хорошую работу.

Основная роль уровня обслуживания заключается в соблюдении принципа разделения интересов и адреса логики команд , связанных с бизнес-функцией.

В реальных приложениях логика запросов часто не нуждается в расширении помимо механики поиска, предоставляемой самим хранилищем (например, изменения значений, преобразования типов).

Рассмотрим два следующих сценария:

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

Оба метода являются простыми расширениями запросов, но, как бы тонко они ни были, они играют разные роли. Первый - это простой фильтр, а второй подразумевает необходимость бизнеса (например, обслуживание или выставление счетов). В простом приложении можно реализовать их оба в хранилище. В более идеалистической системе UsedThisYear лучше всего подходит для уровня обслуживания (и может даже быть реализован как обычный метод экземпляра), где это также может лучше облегчить CQRS стратегию разделения команд и запросов .

Ключевые соображения: (а) основная цель вашего хранилища и (б) насколько вы хотите придерживаться CQRS и DDD философий.

...