• Microsoft .NET
  • C#.NET
  • Решение 11 распространенных проблем в многопоточном коде

Разделение структуры LINQ to SQL - Теоретические основы

ОГЛАВЛЕНИЕ

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

Данная статья использует следующие технологии, о которых вам необходимо знать: 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 и, вероятно, на другие сервисы.