После некоторых исследований и на основе ответов здесь и на этих ссылках я решил полностью изменить дизайн моего API.
Основная концепция заключается в том, чтополностью запретить пользовательские запросы в бизнес-коде.Это решает две проблемы:
- Улучшена тестируемость
- Проблемы, описанные в сообщении Марка , больше не могут возникнуть.Бизнес-уровень больше не нуждается в неявных знаниях о том, какое хранилище данных используется, чтобы узнать, какие операции разрешены в
IQueryable<T>
, а какие нет.
В бизнес-коде запрос теперь выглядит следующим образом:
IEnumerable<Invoice> inv = repository.Query
.Invoices.ThatAre
.Started()
.Unfinished()
.And.WithoutError();
// or
IEnumerable<Invoice> inv = repository.Query.Invoices.ThatAre.Started();
// or
Invoice inv = repository.Query.Invoices.ByInvoiceNumber(invoiceNumber);
На практике это реализовано так:
Как Витаутас Макконис предложил в свой ответ , я больше не зависел напрямую от ISession
NHibernate, вместо этогоЯ сейчас в зависимости от IRepository
.
Этот интерфейс имеет свойство с именем Query
типа IQueries
.Для каждого объекта, к которому должен обращаться бизнес-уровень, существует свойство в IQueries
.Каждое свойство имеет собственный интерфейс, который определяет запросы для объекта.Каждый интерфейс запросов реализует общий интерфейс IQuery<T>
, который, в свою очередь, реализует IEnumerable<T>
, что приводит к очень чистому синтаксису, подобному DSL, который мы видели выше.
Некоторый код:
public interface IRepository
{
IQueries Queries { get; }
}
public interface IQueries
{
IInvoiceQuery Invoices { get; }
IUserQuery Users { get; }
}
public interface IQuery<T> : IEnumerable<T>
{
T Single();
T SingleOrDefault();
T First();
T FirstOrDefault();
}
public interface IInvoiceQuery : IQuery<Invoice>
{
IInvoiceQuery Started();
IInvoiceQuery Unfinished();
IInvoiceQuery WithoutError();
Invoice ByInvoiceNumber(string invoiceNumber);
}
Этот свободный запросСинтаксис позволяет бизнес-уровню объединять предоставленные запросы, чтобы в полной мере использовать возможности базового ORM, чтобы обеспечить максимальную фильтрацию базы данных.
Реализация для NHibernate будет выглядеть примерно так:
public class NHibernateInvoiceQuery : IInvoiceQuery
{
IQueryable<Invoice> _query;
public NHibernateInvoiceQuery(ISession session)
{
_query = session.Query<Invoice>();
}
public IInvoiceQuery Started()
{
_query = _query.Where(x => x.IsStarted);
return this;
}
public IInvoiceQuery WithoutError()
{
_query = _query.Where(x => !x.HasError);
return this;
}
public Invoice ByInvoiceNumber(string invoiceNumber)
{
return _query.SingleOrDefault(x => x.InvoiceNumber == invoiceNumber);
}
public IEnumerator<Invoice> GetEnumerator()
{
return _query.GetEnumerator();
}
// ...
}
В моей реальной реализации я выделил большую часть кода инфраструктуры в базовый класс, так что стало очень легко создать новый объект запроса для новой сущности.Добавить новый запрос к существующему объекту также очень просто.
Приятно то, что бизнес-уровень полностью свободен от логики запросов и, следовательно, хранилище данных можно легко переключать.Или можно реализовать один из запросов с использованием API критериев или получить данные из другого источника данных.Бизнес-уровень не будет обращать внимания на эти детали.