Привет! В этот раз я бы хотел обсудить вопрос компоновки объектов в WCF службе. Пускай у нас будет класс службы, которому для работы необходимо иметь два объекта. Для определенности это будет служба построения отчетов, которой для работы нужен доступ к репозиторию данных и очереди подготовки отчетов. Как правильно передать эти зависимости в класс службы?
Первое, что приходит в голову - внедрение конструктора. Я считаю этот подход самым корректным. Но в WCF c этим могут возникнуть некоторые трудности. Механизм передачи будет зависеть от способа размещения (хостинга), которым мы будем пользоваться.
Из коробки WCF не поддерживает внедрение конструктора. Для создания экземпляра службы хост вызывает конструктор по умолчанию. Если этого конструктора не будет, то во время выполнения будет выброшено исключение ServiceActivationException. Можно ли обойти это ограничение?
Если мы используем автохостинг, то мы можем создать экземпляр службы сами путем вызова необходимого конструктора и далее передать его хосту. Это рабочее решение, только если мы хотим запустить службу в режиме SingleInstanceContextMode, что не всегда подходит для проектируемой службы по ряду причин.
Интерфейс IInstanceProvider определяет, как будут создаваться и уничтожаться экземпляры сервисов. Вот его полное описание:
Первое, что приходит в голову - внедрение конструктора. Я считаю этот подход самым корректным. Но в WCF c этим могут возникнуть некоторые трудности. Механизм передачи будет зависеть от способа размещения (хостинга), которым мы будем пользоваться.
Из коробки WCF не поддерживает внедрение конструктора. Для создания экземпляра службы хост вызывает конструктор по умолчанию. Если этого конструктора не будет, то во время выполнения будет выброшено исключение ServiceActivationException. Можно ли обойти это ограничение?
Если мы используем автохостинг, то мы можем создать экземпляр службы сами путем вызова необходимого конструктора и далее передать его хосту. Это рабочее решение, только если мы хотим запустить службу в режиме SingleInstanceContextMode, что не всегда подходит для проектируемой службы по ряду причин.
При размещении службы в IIS так просто уже все организовать не получится. В этом случае нам помогут многочисленные точки расширения WCF. У WCF есть множество точек расширения, но когда речь заходит о внедрении зависимостей, все, что нам нужно для счастья, - это интерфейс IInstanceProvider и поведения контрактов (Contract Behaviors).
Интерфейс IInstanceProvider определяет, как будут создаваться и уничтожаться экземпляры сервисов. Вот его полное описание:
public interface IInstanceProvider { object GetInstance(InstanceContext context); object GetInstance(InstanceContext context, Message message); void ReleaseInstance(InstanceContext context, object instance); }
Два метода GetInstance отвечают за создание экземпляра сервиса, а ReleaseInstance определяет его удаление при необходимости. Базовая реализация требует наличия конструктора по умолчанию, но мы можем заменить ее на свою, поддерживающую внедрение конструктора.
Рисунок показывает процесс создания экземпляра службы при получении сервером IIS запроса на выполнение контракта операции. Когда поступает запрос, WCF определяет, какой тип реализует необходимый сервис. WCF делает запрос к фабрике ServiceHostFactory на создание подходящего хоста службы. Созданный хост выполняет свою часть работы путем применения поведения (Contract Behavior) и создания требуемого экземпляра службы.
Таким образом, при размещении службы в IIS мы можем реализовать свои версии фабрики хоста и самого хоста, которые будут создавать экземпляр службы со всеми необходимыми зависимостями.
Пускай у нас есть следующее описание службы, которая принимает две зависимости через внедрение конструктора.
public class ReportService : IReportService { private readonly IReportRepository _repository; private readonly IReportPreparationQueue _queue; public ReportService(IReportRepository repository, IReportPreparationQueue queue) { _repository = repository; _queue = queue; } public Guid PostReportToQueue(long id, List<ReportParameter> parameters)... public bool CheckReportIsReady(Guid reportId)... }
Создадим свою версию фабрики хоста, которая будет инициализировать DI-контейнер и передавать его дальше по цепочке.
public class ReportsServiceHostFactory : ServiceHostFactory { private readonly ReportsServiceContainer _container; public ReportsServiceHostFactory() { _container = new ReportsServiceContainer(); } protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses) { if (serviceType == typeof (ReportService)) { return new ReportsServiceHost(_container, serviceType, baseAddresses); } return base.CreateServiceHost(serviceType, baseAddresses); } }
Наша фабрика является наследником ServiceHostFactory, который всю основную работу возьмет на себя. Мы только в конструкторе создаем экземпляр контейнера внедрения зависимостей, который дальше создаст нам экземпляр сервиса. И в методе CreateServiceHost мы создаем экземпляр нашего самодельного хоста со сконфигурированным контейнером, если пришел запрос к службе отчетности.
ReportsServiceHost отвечает за назначение соответствующих поведений всем типам сервисов в хостах. В данном случае мы хотим добавить только одно поведение, которое назначает сервисам наш InstanceProvider.
public class ReportsServiceHost : ServiceHost { public ReportsServiceHost(IReportsServiceContainer container, Type serviceType, params Uri[] baseAddresses) :base(serviceType, baseAddresses) { var contracts = this.ImplementedContracts.Values; foreach (var contract in contracts) { var provider = new ReportsInstanceProvider(container); contract.Behaviors.Add(provider); } } }
Класс ReportsServiceHost является наследником класса ServiceHost, перекладывая на родителя всю работу. Сам же он в конструкторе назначает необходимое поведение всем службам. Свойство ImplementedCotracts возвращает контракты, реализованные службой. Именно через него мы можем добавить поведение в службу.
Последний участник нашей тройки, класс ReportsInstanceProvider реализует интерфейсы IInstanceProvider и IContractBehavior. Через конструктор передается DI-контейнер. В данном примере используется самодельный контейнер ReportsServiceContainer, но его свободно можно заменить любым удобным вам DI-контейнером.
public class ReportsInstanceProvider: IInstanceProvider, IContractBehavior { private readonly IReportsServiceContainer _container; public ReportsInstanceProvider(IReportsServiceContainer container) { _container = container; } public object GetInstance(InstanceContext instanceContext) { return _container.ResolveReportService(); } public object GetInstance(InstanceContext instanceContext, Message message) { return GetInstance(instanceContext); } public void ReleaseInstance(InstanceContext instanceContext, object instance) { _container.Release(instance as IReportService); }
Реализация IInstanceProvider довольна проста. Оба метода GetInstance обращаются к контейнеру для получения нового экземпляра службы отчетов с уже переданными зависимостями. После того как будет выполнен запрос к службе, будет вызван метод ReleaseInstance для очистки ресурсов. Он также делегирует всю работу нашему контейнеру. Как устроен контейнер, можно взглянуть в исходниках.
Оставшаяся часть ReportsInstanceProvider реализует интерфейс IContractBehavior. Мы реализуем интерфейс для того, чтобы добавить его в список поведений, как показано на рисунке выше. Для наших целей нужно реализовать только метод ApplyDispatchBehavior, остальные методы нужно оставить пустыми.
public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime) { dispatchRuntime.InstanceProvider = this; }
WCF вызывает этот метод и передает ему экземпляр DispatchRuntime, что позволяет сообщить, что нужно использовать конкретную реализацию IInstanceProvider. Именно теперь WCF начинает знать, какой метод GetInstance вызывать.
Все, мы добрались до конца! Осталась малость. Нужно научить IIS использовать нашу фабрику. Для этого необходимо привести содержимое файла SVC в соответствие с кодом ниже.
<%@ ServiceHost Language="C#" Service="SamplePoorDIService.ReportService" Factory="SamplePoorDIService.ReportsServiceHostFactory" %>
Этот код указывает IIS, что для создания экземпляров службы необходимо использовать фабрику ReportsServiceHostFactory. Класс ServiceHostFactory должен обязательно иметь конструктор по умолчанию.
Как видите, композиция служб WCF с применением внедрения конструктора - задача не самая тривиальная и довольно трудоемкая. Но у меня есть одна хорошая новость.
Все уже давно написали за нас! Для большинства стандартных DI-контейнеров уже имеются готовые решения для внедрения зависимостей в WCF. В качестве иллюстрации изучите проект UnityDIService из исходников примеров. В данном примере используется проект Unity.Wcf. Как понятно из названия, в качестве контейнера используется Unity. Все делается довольно просто. Библиотека добавляется через NuGet, при добавлении будет сгенерирован класс WcfServiceFactory, в котором необходимо описать все зависимости в контейнере.
public class WcfServiceFactory : UnityServiceHostFactory { protected override void ConfigureContainer(IUnityContainer container) { container .RegisterType<IReportRepository, ReportRepository>() .RegisterType<IReportPreparationQueue, ReportPreparationQueue>() .RegisterType<IReportService, ReportService>(); } }
После этого необходимо привести в порядок SVC файл. После этого все будет работать.
Исходники примеров забирать тут.
Комментариев нет:
Отправить комментарий