Многим разработчикам время от времени приходится взаимодействовать с различными SOAP веб-сервисами. Рассмотрим создание класса, способного обращаться к таким веб-сервисам, и постараемся при этом избежать лишних зависимостей от компонентов WCF и сделать взаимодействие максимально удобным. Имея WSDL веб-сервиса, с которым мы хотим работать, первым делом сгенерируем интерфейс для обращения к нему с помощью утилиты svcutil. Результат будет примерно следующим:
Наш компонент, периодически вызывающий этот веб-сервис, будет выглядеть так:
Готово. MyService может максимально прозрачно работать с веб-сервисом через его интерфейс ISomeService и использует внедрение зависимости. Это делает его прекрасно приспособленным для модульного тестирования и использования других реализаций интерфейса ISomeService, получающих данные из других источников. Осталось только создать реализацию ISomeService и не забыть про особенности обработки исключений при работе с WCF:
Эта реализация обеспечивает необходимое поведение. Однако, при наличии множества веб-сервисов и методов, создание подобных реализаций является морально и физически тяжелой задачей. И тут нам на помощь приходит Castle.DynamicProxy. Для перехвата методов произвольного интерфейса необходимо реализовать интерфейс IInterceptor и переопределить метод Intercept:
Появление дополнительного интерфейса IChannelFactoryProvider обеспечивает возможность создания фабрики каналов ChannelFactory с помощью различных параметров, отличных от endpointConfigurationName из прошлого примера. Нетрудно увидеть в этом применение известного принципа открытости/закрытости из набора SOLID. Реализация IChannelFactoryProvider для Basic-аутентификации может иметь вид:
Осталось собрать все вместе. Для получения реализации нашего интерфейса ISomeService с помощью реализации IInterceptor в библиотеке Castle.DynamicProxy имеется класс ProxyGenerator. С его помощью можно создать экземпляр разрабатываемого класса MyService следующим образом:
Напоследок, рассмотрим создание MyService с помощью контейнера внедрения зависимостей Castle Windsor.
Настройки BasicAuthChannelFactoryProvider при этом можно вынести в конфигурационный файл:
В других DI контейнерах можно добиться аналогичного результата с использованием метода CreateInterfaceProxyWithoutTarget класса ProxyGenerator библиотеки Castle.DynamicProxy.
[ServiceContract]
public interface ISomeService
{
[OperationContract]
Response GetData(Request request);
}
Наш компонент, периодически вызывающий этот веб-сервис, будет выглядеть так:
public class MyService
{
private readonly ISomeService _someServise
public MyService (ISomeService someServise)
{
_someServise = someServise;
}
public void DoSomething()
{
var response = _someServise.GetData();
...
}
}
Готово. MyService может максимально прозрачно работать с веб-сервисом через его интерфейс ISomeService и использует внедрение зависимости. Это делает его прекрасно приспособленным для модульного тестирования и использования других реализаций интерфейса ISomeService, получающих данные из других источников. Осталось только создать реализацию ISomeService и не забыть про особенности обработки исключений при работе с WCF:
public class SomeServiceWcfProxy : ISomeService
{
private readonly ChannelFactory<ISomeService> _channelFactory;
public SomeServiceWcfProxy (string endpointConfigurationName)
{
_channelFactory = new ChannelFactory<ISomeService>(endpointConfigurationName)
}
public Response GetData(Request request)
{
IClientChannel channel = null;
try
{
channel = _channelFactory.CreateChannel();
channel.Open();
var result = channel.GetData(request);
channel.Close();
return result;
}
catch (Exception)
{
if (channel != null)
channel.Abort();
throw;
}
}
}
Эта реализация обеспечивает необходимое поведение. Однако, при наличии множества веб-сервисов и методов, создание подобных реализаций является морально и физически тяжелой задачей. И тут нам на помощь приходит Castle.DynamicProxy. Для перехвата методов произвольного интерфейса необходимо реализовать интерфейс IInterceptor и переопределить метод Intercept:
public class WcfProxyInterceptor<TWcfServiceInterface> : IInterceptor where TWcfServiceInterface : class
{
private readonly ChannelFactory<TWcfServiceInterface> _channelFactory;
public WcfProxyInterceptor(IChannelFactoryProvider<TWcfServiceInterface> channelFactoryProvider)
{
_channelFactory = channelFactoryProvider.GetChannelFactory();
}
public void Intercept(IInvocation invocation)
{
IClientChannel channel = null;
try
{
channel = (IClientChannel)_channelFactory.CreateChannel();
channel.Open();
invocation.ReturnValue = invocation.Method.Invoke(channel, invocation.Arguments);
channel.Close();
}
catch (Exception e)
{
if (channel != null)
channel.Abort();
var ex = e as TargetInvocationException;
if (ex != null)
throw ex.InnerException;
throw;
}
}
}
public interface IChannelFactoryProvider<TWcfServiceInterface> where TWcfServiceInterface : class
{
ChannelFactory<TWcfServiceInterface> GetChannelFactory();
}
Появление дополнительного интерфейса IChannelFactoryProvider обеспечивает возможность создания фабрики каналов ChannelFactory с помощью различных параметров, отличных от endpointConfigurationName из прошлого примера. Нетрудно увидеть в этом применение известного принципа открытости/закрытости из набора SOLID. Реализация IChannelFactoryProvider для Basic-аутентификации может иметь вид:
public class BasicAuthChannelFactoryProvider<TWcfServiceInterface> : IChannelFactoryProvider<TWcfServiceInterface> where TWcfServiceInterface : class
{
private readonly string _endpointConfigurationName;
private readonly string _userName;
private readonly string _password;
public BasicAuthChannelFactoryProvider(string endpointConfigurationName, string userName, string password)
{
_endpointConfigurationName = endpointConfigurationName;
_userName = userName;
_password = password;
}
public ChannelFactory<TWcfServiceInterface> GetChannelFactory()
{
var channelFactory = new ChannelFactory<TWcfServiceInterface>(_endpointConfigurationName);
var clientCredentials = new ClientCredentials();
clientCredentials.UserName.UserName = _userName;
clientCredentials.UserName.Password = _password;
channelFactory.Endpoint.Behaviors.RemoveAll<ClientCredentials>();
channelFactory.Endpoint.Behaviors.Add(clientCredentials);
return channelFactory;
}
}
Осталось собрать все вместе. Для получения реализации нашего интерфейса ISomeService с помощью реализации IInterceptor в библиотеке Castle.DynamicProxy имеется класс ProxyGenerator. С его помощью можно создать экземпляр разрабатываемого класса MyService следующим образом:
var pg = new ProxyGenerator();
var someInterfaceWcfProxy = pg.CreateInterfaceProxyWithoutTarget<ISomeService>(
new WcfProxyInterceptor<ISomeService>(new BasicAuthChannelFactoryProvider<ISomeService>("myEndpoint","user","pass")));
var myService = new MyService(someInterfaceWcfProxy);
Напоследок, рассмотрим создание MyService с помощью контейнера внедрения зависимостей Castle Windsor.
IWindsorContainer container = new WindsorContainer(new XmlInterpreter());
container.Register(Component.For<BasicAuthChannelFactoryProvider<ISomeService>>().Named("basicChannelFactoryProvider"));
container.Register(Component.For<WcfProxyInterceptor<ISomeService>>().Named("myWcfProxy")
.DependsOn((Dependency.OnComponent(typeof(IChannelFactoryProvider<ISomeService>), "basicChannelFactoryProvider"))));
container.Register(Component.For<ISomeService>().Interceptors(InterceptorReference.ForKey("myWcfProxy")).First);
container.Register(Component.For<MyService>());
var myProxy = container.Resolve<MyService>();
Настройки BasicAuthChannelFactoryProvider при этом можно вынести в конфигурационный файл:
<configSections>
<section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
</configSections>
<castle>
<components>
<component id="basicChannelFactoryProvider">
<parameters>
<endpointConfigurationName>myEndpoint</endpointConfigurationName>
<userName>user</userName>
<password>pass</password>
</parameters>
</component>
...
В других DI контейнерах можно добиться аналогичного результата с использованием метода CreateInterfaceProxyWithoutTarget класса ProxyGenerator библиотеки Castle.DynamicProxy.
Vadimyan
Всё равно приходится писать бойлерплейт, регистрировать в контейнере Proxy, не забывать о вспомогательных классах. А ведь можно обойтись совсем без этого:
.mihalagutin
Спасибо, почитаю.
Приведенные класс WcfProxyInterceptor и интерфейс IChannelFactoryProvider не претендуют на полноценную библиотеку. Право создать таковую для своих нужд с возможностью более лаконичной регистрации компонентов в любимом DI контейнере оставляется заинтересованным читателям.
Код регистрации в контейнере действительно выглядит громоздким. В корпоративных приложениях различным компонентам может потребоваться работать с интерфейсом ISomeService, например, с различными учетными данными или типами аутентификации. В этом случае не просто будет ограничится авторегистрацией прокси в контейнере.
Vadimyan
Разная аутентификация к сервисам одного сервиса в рамках одного приложения это интересно, таких требований мы не встречали, обычно всё идёт в рамках сессии пользователя, что и описано. Под более сложные сценарии не реализовывали чего-то.
Да, согласен, в приведенной статье есть лок на Unity, но только потому, что код конфигурации вынесен для удобства в отдельную библиотеку. Здесь можно поступить аналогичным образом, давая коннекторы к разным IoC-контейнерам. По крайней мере, дать хелпер для написания вместо:
примерно этого (простите, не силён в Castle):
И спрятать внутри эти DependsOn ForKey. Понятно, что подобные хелперы для себя делают не только лишь все, но иметь его в готовом виде приятнее.
В любом случае, за статью спасибо.
mihalagutin
Более реалистичный пример необходимости нескольких IChannelFactoryProvider к одному интерфейсу в рамках одного приложения можно представить, если в интерфейсе ISomeInterface есть долгая операция, и нужно увеличить таймаут только у использующего ее компонента.
Насчет хелперов согласен. В тоже время целью статьи было не предоставление готовой библиотеки для какого-либо контейнера или без привязки к нему, а скорее просто желание обратить внимание на то, что нужно стараться добавлять зависимости от веб-сервисов напрямую через их интерфейс, избегая лишних зависимостей.