Главная страница   /   7.3. Построение WCF приложений (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

7.3. Построение WCF приложений

WCF – одна из самых расширяемых составляющих библиотеки стандартных классов (BCL). Несмотря на то, что начать писать WCF сервисы довольно легко, бесчисленные возможности расширяемости могут усложнить процесс поиска той возможности, которая нужна именно вам. Это еще один случай, когда в игру вступает механизм внедрения зависимостей.

Примечание

Согласно шутке, WCF – это акроним для Windows Complication Foundation. В этом утверждении есть определенная доля правды.

Вас легко можно было бы склонить к тому, что WCF не поддерживает внедрение через конструктор. Если вы будете реализовывать WCF сервис при помощи механизма внедрения через конструктор и без конструктора по умолчанию, то хостинг WCF сервиса в рабочей среде будет выдавать ServiceActivationException с сообщением, подобным следующему:

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

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

  • Как мы можем это сделать, если мы размещаем сервис на Internet Information Services (IIS)?
  • Для этого требуется, чтобы сервис запускался в Single InstanceContextMode, который по разным причинам не желателен.

Отличная новость – сообщение об исключении просто вводит нас в заблуждение. Существуют другие способы разрешения внедрения через конструктор в рамках WCF.

Расширяемость WCF

WCF обладает множеством возможностей для расширяемости, но когда дело доходит до механизма внедрения зависимостей, нам всего лишь необходимо иметь представление об интерфейсе IInstanceProvider и о поведениях контрактов. Поведение контракта – это шов в WCF, который позволяет нам изменять то, как ведет себя данный контракт (в данном случае – сервис).

IInstanceProvider – это интерфейс, который определяет, как создаются экземпляры сервиса (и высвобождаются). Ниже приведено определение интерфейса во всей его красе:

public interface IInstanceProvider
{
	object GetInstance(InstanceContext instanceContext);
	object GetInstance(InstanceContext instanceContext, Message message);
	void ReleaseInstance(InstanceContext instanceContext, object instance);
}

Две перегрузки GetInstance ответственны за создание соответствующего экземпляра сервиса, а ReleaseInstance при необходимости предоставляет возможность для очистки.

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

Рисунок 7-7: Когда в сервисную операцию поступает сообщение (запрос), WCF определяет, какой тип CLR реализует сервис. WCF просит ServiceHostFactory создать соответствующий ServiceHost, который может разместить запрашиваемый сервис. ServiceHost выполняет свою часть работы, применяя поведения и создавая запрашиваемый экземпляр.
Когда мы размещаем WCF сервис в IIS, ServiceHostFactory обязателен, несмотря на то, что, если мы явным образом не определим альтернативу, будет использоваться реализация по умолчанию. Если мы размещаем сервис вручную, ServiceHostFactory все еще может быть полезной, но не является необходимой, потому что мы можем создать соответствующий ServiceHost, напрямую в коде.

Когда ServiceHost применяет поведения, он собирает их, по крайней мере, из трех различных мест перед тем, как их объединить:

  • Атрибуты
  • Файл .config
  • Объекты, хранящиеся в оперативной памяти

Несмотря на то, что мы можем определить поведения в атрибутах, это не очень заманчивая для использования стратегия в тех случаях, когда дело касается механизма внедрения зависимостей, поскольку это означает, что мы компилируем в коде конкретную стратегию создания с конкретными зависимостями. Конечный результат почти такой же, как если бы у нас прямо в сервисе присутствовали жестко закодированные зависимости, только гораздо более изощрённее.

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

Объекты, хранящиеся в оперативной памяти, обеспечивают наилучшую гибкость, потому что мы можем выбрать вариант создания зависимостей напрямую в коде или на основании настроек конфигурации. Если мы используем DI-контейнер, мы получаем обе опции бесплатно. Это означает, что мы должны создать пользовательский ServiceHostFactory, который создает экземпляры пользовательского ServiceHost, который снова может соединить нужный сервис со всеми его зависимостями.

Мы можем создать набор универсальных классов, которые делают это на основании выбранного DI-контейнера, или используют один из уже реализованных, многократно используемых ServiceHostFactorys. Также мы можем создать специальный ServiceHostFactory для конкретного сервиса. Поскольку создание специального ServiceHostFactory является наилучшей иллюстрацией процесса, в следующем примере используется специализированная фабрика.

Пример: подключение сервиса управления продуктами

В качестве примера представьте себе, что вас попросили расширить шаблонное приложение Commerce WCF сервисом, который раскрывает операции, позволяющие другим приложениям управлять данными товара. Это позволяет вам вмонтировать богатого по функциям клиента (вы будете делать это в следующем разделе) или пакетное задание для того, чтобы управлять данными товара.

Знакомство с сервисом управления товарами

Для упрощения примера давайте предположим, что вы хотите раскрыть простые операции Create, Read, Update и Delete (CRUD). Рисунок 7-8 демонстрирует диаграмму сервисов и связанных контрактов данных.

Рисунок 7-8: IProductManagementService – это WCF сервис, который определяет простые CRUD операции для товаров. Он использует связанные ProductContract и MoneyContract для раскрытия этих операций. Несмотря на то, что это не продемонстрировано на диаграмме, все три типа помечены обычными WCF атрибутами: ServiceContract, OperationContract, DataContract и DataMember.

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

Подсказка

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

Доменная модель представляет товар как Entity Product, а сервисный контракт раскрывает его операции в терминах Data Transfer Object (DTO) ProductContract. Для преобразований между этими двумя различными типами вы также вводите интерфейс под названием IContractMapper.

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

public ProductManagementService(ProductRepository repository,
	IContractMapper mapper)

До настоящего времени мы счастливо игнорировали слона в комнате: как нам получить WCF для того, чтобы корректно подключить экземпляр ProductManagementService?

Присоединение ProductManagementService к WCF

Как показано на рисунке 7-7, Composition Root в WCF – это триплет ServiceHostFactory, ServiceHost и IInstanceProvider. Чтобы подключить к сервису механизм внедрения через конструктор, мы должны обеспечить пользовательские реализации всех трех этих компонентов.

Подсказка

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

Примечание

В данном примере реализуется контейнер, жестко присоединенный при помощи Poor Man's DI. Я решил инкапсулировать жестко закодированные зависимости в пользовательском классе контейнера для того, чтобы дать вам хорошее представление о том, как создать многократно используемое решение на основании конкретного DI-контейнера.

Entity vs. DTO

Предыдущий параграф подбросил вам немного жаргона, поэтому давайте вкратце рассмотрим, что подразумевается под Entity и DTO.

Entity – это термин проблемно-ориентированного проектирования (Domain-Driven Design), который охватывает объект Domain, имеющий долгосрочную идентификацию, которая не относится к конкретному экземпляру объекта. Это может показаться абстрактным и теоретическим, но это означает, что Entity представляет собой объект, который живет за пределами произвольных битов памяти. Любой экземпляр .NET объекта имеет внутренний адрес (идентификацию), но Entity обладает идентификацией, которая обитает по ту сторону жизненного процесса. Мы часто используем базы данных и первичные ключи для идентификации Entities и для того, чтобы убедиться, что мы можем сохранять и читать их, даже если хост-компьютер будет перезагружен.

Доменный объект Product – это Entity, поскольку у сущности товара гораздо более продолжительный жизненный цикл, нежели у единичного процесса, и мы используем ID товара для его идентификации в ProductRepository.

Data Transfer Object (DTO), с другой стороны, существует только для того, чтобы быть переданным с одного уровня приложения на другой. Несмотря на то, что Entity может инкапсулировать большую часть поведения, DTO – это структура данных без поведения.

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

Давайте начнем с пользовательского ServiceHostFactory, который является настоящей точкой входа в WCF сервис. Следующий листинг демонстрирует реализацию.

Листинг 7-4: Пользовательский ServiceHostFactory
public class CommerceServiceHostFactory : ServiceHostFactory
{
	private readonly ICommerceServiceContainer container;
	public CommerceServiceHostFactory()
	{
		this.container =
			new CommerceServiceContainer();
	}
	protected override ServiceHost CreateServiceHost(
		Type serviceType, Uri[] baseAddresses)
	{
		if (serviceType == typeof(ProductManagementService))
		{
			return new CommerceServiceHost(
				this.container,
				serviceType, baseAddresses);
		}
		return base.CreateServiceHost(serviceType, baseAddresses);
	}
}

Строка 6-7: Создает экземпляр контейнера

Строка 14-16: Создает пользовательский ServiceHost

Пользовательский CommerceServiceHostFactory унаследован от ServiceHostFactory с единственной целью – присоединить экземпляры ProductManagementService. Он использует пользовательский CommerceServiceContainer для выполнения реальной работы, поэтому создает экземпляр контейнера в его конструкторе. Вы можете легко расширить этот пример для того, чтобы использовать настоящий DI-контейнер, создавая и конфигурируя вместо этого экземпляр данного контейнера.

При получении запроса о создании ServiceHost он возвращает новый CommerceServiceHost с настроенным контейнером, если запрашиваемый тип сервиса подходит. CommerceServiceHost ответственен за определение подходящих поведений для всех типов сервисов, на которых он размещается. В данном примере вы хотите добавить всего лишь одно поведение, которое передает желаемый IInstanceProvider в сервисы. Вы можете завершить всю эту работу в конструкторе, продемонстрированном ниже, а базовый класс позаботится обо всем остальном.

Листинг 7-5: Пользовательский ServiceHost
public class CommerceServiceHost : ServiceHost
{
	public CommerceServiceHost(ICommerceServiceContainer container,
		Type serviceType, params Uri[] baseAddresses)
		: base(serviceType, baseAddresses)
	{
		if (container == null)
		{
			throw new ArgumentNullException("container");
		}
		var contracts = this.ImplementedContracts.Values;
		foreach (var c in contracts)
		{
			var instanceProvider =
				new CommerceInstanceProvider(
					container);
			c.Behaviors.Add(instanceProvider);
		}
	}
}

Строка 14-16: Создает InstanceProvider

Строка 17: Добавляет InstanceProvider в качестве поведения

Класс CommerceServiceHost наследуется от ServiceHost, который является конкретным классом, выполняющим всю тяжелую работу. В большинстве случаев вы будете размещать только один вид сервиса (в данном случае, ProductManagementService), но вам разрешено размещать самые разнообразные сервисы; это означает, что вы должны добавить ко всем этим сервисам IInstanceProvider. Свойство ImplementedContracts – это словарь, поэтому вы можете выполнить цикл по его Values, чтобы пометить их всех.

Для каждого вида сервиса вы инициализируете новый экземпляр пользовательского класса CommerceInstanceProvider вместе с контейнером. Поскольку он дублируется в качестве поведения, вы можете добавить его в Behaviors сервиса.

Последней составляющей пользовательского WCF триплета является CommerceInstanceProvider, который дублирует как IInstanceProvider, так и IContractBehavior. Это простая реализация, но поскольку она реализует два различных интерфейса со сложными сигнатурами, она может выглядеть слегка устрашающей, если вы видите ее впервые. Вместо нее я продемонстрирую код, отнимающий мало времени; рисунок 7-9 предоставляет краткий обзор.

Рисунок 7-9: CommerceInstanceProvider реализует как IInstanceProvider, так и IContractBehavior, поэтому вам нужно реализовать семь методов. Вы можете оставить три этих метода пустыми, а остальные четыре являются однострочными.

Листинг 7-6 демонстрирует объявление класса и конструктор. Здесь ничего не происходит, кроме использования Constructor Injection для внедрения контейнера. Обычно мы используем механизм внедрения через конструктор, для того, чтобы объявить DI-контейнеру, что классу нужны некоторые зависимости, но в данном случае это делать поздно, поскольку вы внедряете сам контейнер. Это обычно попахивает большим кодом, потому что он чаще всего указывает на намерение использовать анти-паттерн Service Locator, но здесь это необходимо, поскольку вы реализуете Composition Root.

Листинг 7-6: Объявление класса CommerceInstanceProvider и конструктор
public partial class CommerceInstanceProvider :
	IInstanceProvider, IContractBehavior
{
	private readonly ICommerceServiceContainer container;
	public CommerceInstanceProvider(
		ICommerceServiceContainer container)
	{
		if (container == null)
		{
			throw new ArgumentNullException("container");
		}
		this.container = container;
	}
}

Строка 2: Реализует WCF интерфейсы

Строка 4-13: Constructor Injection

CommerceInstanceProvider реализует как IInstanceProvider, так и IContractBehavior. Вы дополняете контейнер посредством стандартного Constructor Injection. В данном примере вы используете пользовательский CommerceServiceContainer, но замена его универсальным DI-контейнером – обычная практика.

Реализация IInstanceProvider в следующем листинге используется рабочей средой WCF для создания экземпляров класса ProductManagementService.

Листинг 7-7: Реализация IInstanceProvider
public object GetInstance(InstanceContext instanceContext, Message message)
{
	return this.GetInstance(instanceContext);
}
public object GetInstance(InstanceContext instanceContext)
{
	return this.container
		.ResolveProductManagementService();
}
public void ReleaseInstance(InstanceContext instanceContext,
	object instance)
{
	this.container.Release(instance);
}

Строка 3: Делегирует полномочия перегрузке

Строка 7-8: Использует контейнер для преобразования

Строка 13: Просит контейнер высвободить экземпляр

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

При завершении операции сервиса рабочая среда WCF просит вас высвободить экземпляр, и вы снова делегируете эту работу контейнеру.

Остальная часть CommerceInstanceProvider – это реализация IContractBehavior. Единственная причина для реализации этого интерфейса – позволить вам добавить его в список поведений, как это продемонстрировано в листинге 7-5. Все методы интерфейса IContractBehavior возвращают void, поэтому вы можете оставить большинство из них пустыми, поскольку вам не нужно их реализовывать.

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

Листинг 7-8: Основная реализация IContractBehavior
public void ApplyDispatchBehavior(
	ContractDescription contractDescription, ServiceEndpoint endpoint,
	DispatchRuntime dispatchRuntime)
{
	dispatchRuntime.InstanceProvider = this;
}

В этом методе вам нужно сделать всего лишь одну очень простую вещь. Рабочая среда WCF вызывает этот метод и передает экземпляр DispatchRuntime, который позволяет вам сказать методу о том, что он должен использовать эту конкретную реализацию IInstanceProvider – помните, что CommerceInstanceProvider также реализует IInstanceProvider. Рабочая среда WCF теперь знает, какой IInstanceProvider использовать, и может впоследствии вызвать метод GetInstance, продемонстрированный в листинге 7-7.

Кажется, чтобы разрешить механизм внедрения зависимостей для WCF необходимо реализовать большое количество кода, а я даже не показал вам реализацию CommerceServiceContainer.

Подсказка

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

Контейнер – это последний фрагмент WCF DI паззла.

Реализация специализированного контейнера

CommerceServiceContainer – это специализированный контейнер с единственной целью присоединения класса ProductManagementService. Помните, что для этого класса нужны экземпляры ProductRepository и IContractMapper в качестве зависимостей.

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

Примечание

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

Метод ResolveProductManagementService связывает экземпляр с Poor Man's DI, что продемонстрировано ниже.

Листинг 7-9: Преобразование ProductManagementService
public IProductManagementService ResolveProductManagementService()
{
	string connectionString =
		ConfigurationManager.ConnectionStrings
		["CommerceObjectContext"].ConnectionString;
	ProductRepository repository =
		new SqlProductRepository(connectionString);
	IContractMapper mapper = new ContractMapper();
	return new ProductManagementService(repository,
		mapper);
}

Строка 6-7: Создает репозиторий товаров

Строка 8: Создает преобразователь контрактов

В некотором смысле, когда дело доходит до преобразования диаграммы зависимостей, часто бывает выгодно вернуться назад. Вы знаете, что вам нужно вернуть экземпляр ProductManagementService с экземплярами ProductRepository и IContractMapper. Экземпляр IContractMapper легко создать, но для того, чтобы создать ProductRepository нужно потрудиться побольше.

Вам хотелось бы использовать SqlProductRepository, но для этого вам нужна строка соединения, которую вы можете прочитать из файла web.config.

Если вы хотите разместить сервис в вашем собственном приложении, то сейчас вы можете это сделать путем создания нового экземпляра класса CommerceServiceHostFactory и вызова его метода CreateServiceHost с корректными параметрами. Метод вернет экземпляр CommerceServiceHost, который вы можете открыть, и будет выполнять остальную часть работы за вас, а также размещать ProductManagementService.

Тем не менее, если вы хотите разместить сервис на IIS, то вы должны выполнить еще один шаг.

Хостинг ProductManagementService в IIS

В IIS мы не создаем новые экземпляры CommerceServiceHostFactory вручную. Вместо этого мы должны сообщить IIS о том, чтобы он сделал это за нас. Это может быть сделано в .svc файле путем добавления атрибута Factory:

<%@ ServiceHost=""
	Factory = "Ploeh.Samples.CommerceService.CommerceServiceHostFactory,
	➥Ploeh.Samples.CommerceService"
	Service = "Ploeh.Samples.CommerceService.ProductManagementService"
%>

Данный .svc файл дает IIS указание использовать CommerceServiceHostFactory всякий раз, когда ему нужно создавать экземпляр класса ProductManagementService. То, что рассматриваемый ServiceHostFactory имеет конструктор по умолчанию, является условием, но в данном примере это именно так.

Разрешение DI в WCF сложнее, нежели это должно было быть, но, по крайней мере, это возможно, и конечного результата вполне достаточно. Мы можем использовать любой желаемый DI-контейнер, и завершаем работу, приобретая правильный Composition Root.

Некоторые фреймворки не предоставляют нам подходящие швы, которые позволили бы нам такую роскошь. Тем не менее, до того, как мы рассмотрим один такой известный фреймворк, давайте расслабимся и рассмотрим более простой фреймворк.