Разделение структуры LINQ to SQL

ОГЛАВЛЕНИЕ

Данная статья расширяет идею комбинации внедрения зависимости (Dependency Injection) с LINQ to SQL. Структура, определенная в данной статье использует структуру внедрения зависимости компании Майкрософт, названную Unity, а также простую версию AOP, называемую Policy Injection (Внедрение политики). Обе эти структуры можно найти в библиотеке Microsoft Enterprise Library 4+. Используя внедрение зависимости мы можем положиться на IoC, что создаст объекты, зависимости которых будут уже установлены. В таком случае мы обеспечим только то, что один DataContext (или в нашем случае IDataContext) будет использован между любыми созданными сущностями или сервисами. Таким образом нам не нужно управлять масштабом DataContext вручную.

Поскольку все объекты в данной структуре будут проинформированы о IDataContext благодаря внедрению зависимости, мы можем добавить несколько полезных методов к нашим сущностям, такие как "Save" (к примеру Article.Save();). Многие иногда используют EntitySpaces и им нравится этот синтаксис в отличии от LINQ to SQL. Используя Unity в качестве структуры внедрения зависимости, мы можем определить уровень данных в файле конфигурации. Конфигурация существенным образом сопоставляет интерфейсы с объектами, что означает возможность очень простого изменения типа объекта, который мы хотим установить. Это означает, что мы можем создать макет уровня данных, или с легкостью проецировать на совершенно  другой уровень данных.

Мы добавили ко всему этому PolicyInjection (внедрение политики) поскольку из-за того, как структура будет собрана, данное внедрение можно будет легко реализовать. Это позволит произвести простую регистрацию, основанную на методах, и кэширование (среди всего прочего). Это хороший пример комбинации внедрения зависимости с внедрением политики.

Исходный проект основан на тестах NUnit.


Теоретические основы

Данная статья использует следующие технологии, о которых вам необходимо знать: LINQ to SQL, Dependency Injection, Policy Injection (AOP).

На самом деле не так легко использовать внедрение зависимости (Dependency Injection) с LINQ to SQL поскольку наиболее важные вещи не снабжены интерфейсом - к примеру, классы System.Data.Linq.DataContext и System.Data.Linq.Table<>. Данная статья может быть названа: Принуждение LINQ to SQL к использованию интерфейсов. Для этого мы создали интерфейс IDataContext. Он содержит все свойства и методы, которые находятся в System.Data.Linq.DataContext , и которые могут быть реализованы при помощи специализированных классов:

int ExecuteCommand(string command, params object[] parameters);
IEnumerable<TResult> ExecuteQuery<TResult>(string query, params object[] parameters);
IEnumerable ExecuteQuery(Type elementType, string query, params object[] parameters);
DbCommand GetCommand(IQueryable query);
ITable GetTable(Type type);
MetaModel Mapping { get; }
void SubmitChanges();
IEnumerable<TResult> Translate<TResult>(DbDataReader reader);
IEnumerable Translate(Type elementType, DbDataReader reader);

Вы заметите, что метод GetTable<T> отсутствует в списке. Это потому, что данный метод не может быть реализован другими классами, поскольку не существует прямого способа создания System.Data.Linq.Table<>. Майкрософт публиковал интерфейс ITable , который содержит основные методы, необходимые таблице, но тем не менее, это не IEnumerable<T> и не может быть использовано для написания LINQ-запросов. Итак, чтобы передать функциональность обоих ITable и IEnumerable<T> в IDataContext одним методом был создан другой метод:

IEnumerableTable<T> GetITable<T>() where T : class;

Данный метод раскрывает специализированный интерфейс, который расширяет ITable и IEnumerable<T>. Теперь мы можем вызвать IDataContext.GetITable<T>() для осуществления запросов по таблицам, при этом вызывать методы ITable (такие, как InsertOnSubmit) по отношению к полученному объекту.

В данной статье мы создадим простой контекст XML-данных (XDataContext) для получения и хранения данных в XML-файлах вместо базы данных SQL. Все будет довольно просто, поэтому не все члены интерфейсов будут реализованы. Все делается в целях демонстрации того, как это можно реализовать.

Для простоты используемая структура данных будет состоять из Members (члены), Articles (статьи) и Comments (комментарии).

Установка DataContext

Для того, чтобы связать сгенерированный LINQ to SQL DataContext с IDataContext нам необходимо создать частичный класс для созданного класса и убедиться, что он реализует IDataContext. Поскольку созданный DataContext не содержит определения для GetITable<T>, то нам также необходимо выполнить это:

public IEnumerableTable<T> GetITable<T>() where T : class
{
    return new EnumerableTable<T>(this.GetTable(typeof(T)));
}

Класс EnumerableTable на самом деле просто класс-капсула для экспозиции ITable и IEnumerable<T>. Таким образом мы можем все это выполнить без того, чтобы создавать экземпляр объекта LINQ Table<>.

Установка классов моделей данных

Каждое определение модели сущности реализуется путем создания интерфейса, просто определяющего свойства модели, и затем интерфейс унаследует IBaseEntity, раскрывающую зависимость от IDataContext, и основные методы, которые должны быть включены в сущность (такие, как Save и Delete). Каждая сущность LINQ to SQL затем должна реализовать модель путем создания частичного класса для нее. Затем, для того, чтобы внедрение зависимости сработало, создается метод-конструктор для классов LINQ to SQL, который обладает объектом IDataContext в качестве параметра (когда Unity создает объект, он ищет конструктор с наибольшим числом параметров и, поскольку новый конструктор имеет больше параметров, чем созданный по умолчанию, Unity знает, что IDataContext является зависимостью).

В данном проекте IArticle, IMember, и IComment будут созданы вручную и частичные классы для Article, Member и Comment должны быть созданы для обеспечения реализации интерфейсов классами LINQ to SQL.

Вот диаграмма классов, демонстрирующая расстановку сущностей:


Установка классов сервисного уровня

Сервис устанавливается для экспозиции метода чтобы взаимодействовать с данными для каждой таблицы. Интерфейс сервиса должен быть создан для того, чтобы определить любую функцию данных, которая должна быть выполнена. Затем она должна унаследовать IBaseService<> , который раскрывает зависимость от IEntityServiceFactory ( в свою очередь являющуюся ссылкой на IDataContext и все другие информационные сервисы). Как только интерфейс будет установлен, то будет созджан сервис в качестве класса. Каждый класс наследует BaseService<> , который уже определяет основные свойства и методы. Для того, чтобы внедрение зависимости работало создается конструктор для каждого сервиса, при этом объект IEntityServiceFactory является его параметром.

Вот диаграмма классов, демонстрирующая установку информационных сервисов:


Установка конфигурации

Секция конфигурации для Unity определяет контейнеры IoC. Каждый контейнер связывает интерфейсы с реальными объектами и в каждой связке мы можем указать время жизни объекта, который создается внедрением зависимости (Dependency Injection). Поскольку мы всего лишь хотим создать один DataContext для контейнера LINQ to SQL, мы можем определить его в качестве одноэлементного множества. Это связывает IDataContext со множеством объекта, созданного LINQ to SQL.

<type type="IDataContext" mapTo="LinqUnity.Linq.DataContext, LinqUnity">
  <lifetime type="singleton"/>
    <typeConfig
      extensionType="Microsoft.Practices.Unity.Configuration.TypeInjectionElement,
        Microsoft.Practices.Unity.Configuration">
      <constructor/>
      <!-- Убедитесь, что он создан стандартным конструктором, не имеющим параметров -->
    </typeConfig>
</type>

Теперь нам необходимо связать интерфейсы модели данных с реальными объектами - в данном случае созданные классы LINQ to SQL.

<type type="LinqUnity.Model.IMember, LinqUnity" 
    mapTo="LinqUnity.Linq.Member, LinqUnity"/>
<type type="LinqUnity.Model.IComment, LinqUnity"
    mapTo="LinqUnity.Linq.Comment, LinqUnity"/>
<type type="LinqUnity.Model.IArticle, LinqUnity"
    mapTo="LinqUnity.Linq.Article, LinqUnity"/>

И наконец, нам необходимо установить информационные сервисы. Синтаксис сложно изучить, но так был создан синтаксис string для определения обобщенных типов. На самом деле IBaseService<T> связывается с реальным сервисом. К примеру, первая связка происходит между IBaseService<Member> и MemberService.

<!-- Управляемый синтаксис является стандартом для обобщенных типов -->
<type
    type="TheFarm.Data.Linq.IBaseService`1[[LinqUnity.Linq.Member, LinqUnity]],
         TheFarm.Data.Linq"
    mapTo="LinqUnity.Service.MemberService, LinqUnity"/>
<type
    type="TheFarm.Data.Linq.IBaseService`1[[LinqUnity.Linq.Comment, LinqUnity]],
         TheFarm.Data.Linq"
    mapTo="LinqUnity.Service.CommentService, LinqUnity"/>
<type
    type="TheFarm.Data.Linq.IBaseService`1[[LinqUnity.Linq.Article, LinqUnity]],
         TheFarm.Data.Linq"
    mapTo="LinqUnity.Service.ArticleService, LinqUnity"/>

Объект EntityServiceFactory

Теперь нам необходимо, чтобы инъекция зависимости создала все объекты за нас. Имея указанную выше конфигурацию, контейнер IoC даст нам объект LinqUnity.Linq.DataContext при запросе IDataContext, объект LinqUnity.Linq.Article при запросе IArticle и т.д.. Чтобы это работало был создан класс EntityServiceFactory , который имеет методы, заставляющие Unity создавать объекты за нас:

  • TCreateEntity<T>() создает новый экземпляр указанного типа.
  • TQuery GetService<TEntity, TQuery>() создает сервис данных с интерфейсом типа TQuery и сущностью типа TEntity.
  • TBuildEntity<T>(T entity) который повторно связывает существующий объект со всеми его зависимостями. В таком случае сущности зависят от IDataContext.

EntityServiceFactory реализует IEntityServiceFactory , которое, как вы заметили, является свойством IBaseService<T> и поэтому зависимостью, поскольку он является параметром конструктора каждого сервиса данных. Конфигурация XML не определяет связку с IEntityServiceFactory, поэтому в этом случае внедрение зависимости (Dependency Injection) не сможет связать все объекты. Тем не менее, когда создается EntityServiceFactory , он вставляет себя в контейнер, который был выделен из Unity во время выполнения в качестве одноэлементного множества:

container.RegisterInstance<IEntityServiceFactory>(this, new ContainerControlledLifetimeManager());

Связка может также быть определена в XML, но тогда другой специализированный класс должен быть создан для создания объектов Unity и т.д. Мы хотели сделать EntityServiceFactory обычным объектом для использования в структуре, тем самым реализация данной структуры не требовала информации о Unity. Стандартный конструктор для EntityServiceFactory загрузит контейнер, определенный в файле конфигурации, называемый DataLayer. В качестве альтернативы вы можете передать различные имена контейнеров перегруженному методу конструктора.

Каждый сервис зависит от IEntityServiceFactory , поскольку каждый сервис может потребовать ссылку на IDataContext и, вероятно, на другие сервисы.


Использование кода

Реализация сервисов данных

Обычно при использовании LINQ to SQL вам необходимо писать запросы, основанные на таблице свойств, сгенерированной по DataContext, к примеру :

var article = myDataContext.Articles.Where(x => x.ArticleId == 1).SingleOrDefault();

или:

var article = myDataContext.GetTable<Article>().
     Where(x => x.ArticleId == 1).SingleOrDefault()

Это не может быть выполнено с данной структурой поскольку ни Article, ни GetTable<T> не являются членами IDataContext. Вместо этого нам необходимо использовать специализированный метод GetITable<T> , который был создан для того, чтобы показать объект IEnumerable<T> запросу :

var article = myDataContext.GetITable<Article>().
     Where(x => x.ArticleId == 1).SingleOrDefault()

При наличии указанного выше синтаксиса наши методы сервисов данных могут выглядеть следующим образом:

public List<IMember> GetMemberStartingWith(char c)
{
    return (from m in this.Factory.DataContext.GetITable<Member>()
            where m.Name.StartsWith(c.ToString())
            select (IMember)m)
            .ToList();                
}

Как оговаривалось в начале статьи, один из недостатков данного подхода заключается в том, что мы не запрашиваем напрямую по System.Data.Linq.Table<T>, поэтому мы теряем возможность использовать дополнительные методы расширения, доступные объекту System.Data.Linq.Table<T> по сравнению с объектом IEnumerable<T>.
Экспозиция сервисов данных

EntityServiceFactory включает в себя основные методы создания сервисов и сущностей со всеми привязанностями уже установленными. Но, тем не менее, более элегантная реализация заключается в расширении данного класса и экспозиции свойств для получения доступа к каждому сервису данных. В данном примере этот класс называется ServiceFactory и он достаточно прост, обладая свойствами: CommentService, ArticleService и MemberService. Каждый вызов к любому из этих свойств возвратит новый объект сервиса, созданный из Dependency Injection (внедрения зависимости). В наиболее простой форме одно из свойств может выглядеть так:

public IArticleService ArticleService
{
    get
    {
        return this.GetService<Article, IArticleService>();
    }
}

Внедрение политики (Policy Injection)

Внедрение политики (Policy Injection) - это простой тип AOP- структуры (автономной платформы объектов) , которую можно найти в библиотеке Microsoft Enterprise Library. В данном примере мы будем использовать внедрение политики (Policy Injection) для выполнения регистрации и кэширования на уровне метода путем простого добавления атрибутов к методам, которые необходимо кэшировать или регистрировать. Для реализации внедрения политики мы изменим код указанных выше свойств на:

public IArticleService ArticleService
{
    get
    {
        IArticleService service = this.GetService<Article, IArticleService>();
        return PolicyInjection.Wrap<IArticleService>(service);
    }
}

Внедрение политики (Policy Injection) требует от объекта расширения MarshalByRefObject или чтобы оно реализовало интерфейс, содержащий методы, которые будут использованы во внедрении политики. Поскольку все наши классы имеют интерфейсы, это будет легко.

Для того, чтобы кэшировать выходной результат метода, вам понадобится добавить CachingCallHandler:

[CachingCallHandler(0, 5, 0)]
public new List<IComment> SelectAll()
{
    return base.SelectAll()
        .Cast<IComment>()
        .ToList();
}

Теперь выходной результат SelectAll() будет кэширован на 5 минут. Регистрация настолько же легка, тем не менее она требует некоторые записи в файле конфигурации:

[LogCallHandler(BeforeMessage = "Begin", AfterMessage = "End")]
public IMember CreateNew(string name, string email, string phone)
{
    Member member = this.Factory.CreateEntity<Member>();
    member.Name = name;
    member.Email = email;
    member.Phone = phone;
    member.DateCreated = DateTime.Now;
    return (IMember)member;
}

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

Добавление атрибутов несложно, и вы также можете настроить внедрение политики (Policy Injection) в конфигурационном файле  так, чтобы он динамически изменял то, что кэшируется, записывается и т.д., без необходимости в повторной компиляции. Тем не менее, целевые методы должны существовать внутри объекта, который упакован или создан при помощи внедрения политики (Policy Injection).

Использование сервисов данных

Все, что вам необходимо сделать, это использовать сервисы данных для создания ServiceFactory и получения доступа к свойствам для вызова соответствующих методов. Это создаст новый IMember:

ServiceFactory factory = new ServiceFactory();
IMember newMember = factory.MemberService
    .CreateNew("Shannon", "Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript.", "12345676");

За занавесом будет создан новый объект Member, а также будет вызван метод InsertOnSubmit его соответствующего члена ITable. Для того, чтобы сохранить изменения DataContext, мы можем просто вызвать:

newMember.Save();

Вызов factory.DataContext.SubmitChanges() выполнит то же самое (но, вышеуказанное кажется более элегантным), а LINQ to SQL не предлагает красивого способа выполнить обновление сущности или таблицы, оно просто обновляет все выполненные изменения, поэтому метод Save() на самом деле просто оболочка для DataContext.SubmitChanges().

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

IMember newMember = memberService.CreateNew("My Name", 
     "Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript.", "00000000");
IArticle newArticle = articleService.CreateNew("My Article",
     "Some text...", "Some description...");

//создание 20-и новых комментариев при помощи созданных выше IMember и  IArticle
List<IComment> comments = new List<IComment>();
for (int i = 0; i < 20; i++)
    comments.Add(commentService.CreateNew("My Comment", newMember, newArticle));

//одновременное сохранение всех новых комментариев в базе данных
factory.DataContext.SubmitChanges();

Использование внедрения зависимости (Dependency Injection) для связывания с альтернативными контекстами данных

Как уже отмечалось в начале статьи, мы создали тестовый контекст данных названный XDataContext , который хранит данные в XML-файлах вместо базы данных. Мы определили второй контейнер в файле конфигурации, который является таким же как и SQL-контейнер. Тем не менее, IDataContext связан с этим XDataContext вместо того, чтобы быть связанным с DataContext от LINQ to SQL . Мы не создавали специализированные сущности поскольку сущности LINQ to SQL не так сложно создать и они уже обрабатывают отношения между сущностями.

Для того, чтобы использовать другие контейнеры, нам необходимо выстроить EntityServiceFactory с названием контейнера.

XDataContext управляет установкой идентификации и приращением, а также отслеживанием дополнений или удаления.

Пункты повышенного интереса

Полезные методы, такие как Delete() и Save() , которые теперь существуют в этих сущностях, имеют некий подвох. Использование метода EntityServiceFactory's CreateEntity<T> для создания сущности автоматически привязывает зависимости сущности с IDataContext, тем самым Save() и Delete() могут быть вызваны. Тем не менее, когда данные сущности будут возвращены из источника данных, они не будут обладать установленными зависимостями. Для того, чтобы это было иначе, необходимо использовать метод BuildEntity<T> из EntityServiceFactory для связи зависимостей с каждым объектом. Это, скорее всего, привнесет некоторое снижение производительности. К примеру, метод SelectAll():

public virtual List<T> SelectAll()
{
    List<T> entities = (from x in Factory.DataContext.GetITable<T>() select x).ToList();
    entities.ForEach(x => Factory.BuildEntity<T>(x));
    return entities;
}

вызывает BuildEntity для каждого элемента, возвращенного из источника данных. Представьте себе сотни тысяч кортежей - в этом случае цена будет гораздо больше. Тем не менее, несмотря на снижение производительности со стороны BuildEntity, оно не настолько значительно по сравнению с использованием стандартного LINQ to SQL со множеством итераций.

С другой стороны, в некоторых источниках указывается , что сериализация сущностей LINQ to SQL к XML невозможна без хитростей, потому для данного примера мы просто реализовали IXmlSerializable и специализировано упорядочили данные объекты.

Вывод

Мы продемонстрировали вам некоторые передовые методы. Было интересно пробовать обойти структуру классов LINQ to SQL для реализации макета сервисов без использования некоторого рода библиотеки макетов.

Рекомендуется скачать исходный код и разобраться в том, что на самом деле происходит.

Shannon Deminick