[Из песочницы] Внедрение WCF-клиентов c помощью Castle.DynamicProxy
Многим разработчикам время от времени приходится взаимодействовать с различными SOAP веб-сервисами. Рассмотрим создание класса, способного обращаться к таким веб-сервисам, и постараемся при этом избежать лишних зависимостей от компонентов WCF и сделать взаимодействие максимально удобным. Имея WSDL веб-сервиса, с которым мы хотим работать, первым делом сгенерируем интерфейс для обращения к нему с помощью утилиты svcutil. Результат будет примерно следующим:
[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 _channelFactory;
public SomeServiceWcfProxy (string endpointConfigurationName)
{
_channelFactory = new ChannelFactory(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 : IInterceptor where TWcfServiceInterface : class
{
private readonly ChannelFactory _channelFactory;
public WcfProxyInterceptor(IChannelFactoryProvider 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 where TWcfServiceInterface : class
{
ChannelFactory GetChannelFactory();
}
Появление дополнительного интерфейса IChannelFactoryProvider обеспечивает возможность создания фабрики каналов ChannelFactory с помощью различных параметров, отличных от endpointConfigurationName из прошлого примера. Нетрудно увидеть в этом применение известного принципа открытости/закрытости из набора SOLID. Реализация IChannelFactoryProvider для Basic-аутентификации может иметь вид:
public class BasicAuthChannelFactoryProvider : IChannelFactoryProvider 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 GetChannelFactory()
{
var channelFactory = new ChannelFactory(_endpointConfigurationName);
var clientCredentials = new ClientCredentials();
clientCredentials.UserName.UserName = _userName;
clientCredentials.UserName.Password = _password;
channelFactory.Endpoint.Behaviors.RemoveAll();
channelFactory.Endpoint.Behaviors.Add(clientCredentials);
return channelFactory;
}
}
Осталось собрать все вместе. Для получения реализации нашего интерфейса ISomeService с помощью реализации IInterceptor в библиотеке Castle.DynamicProxy имеется класс ProxyGenerator. С его помощью можно создать экземпляр разрабатываемого класса MyService следующим образом:
var pg = new ProxyGenerator();
var someInterfaceWcfProxy = pg.CreateInterfaceProxyWithoutTarget(
new WcfProxyInterceptor(new BasicAuthChannelFactoryProvider("myEndpoint","user","pass")));
var myService = new MyService(someInterfaceWcfProxy);
Напоследок, рассмотрим создание MyService с помощью контейнера внедрения зависимостей Castle Windsor.
IWindsorContainer container = new WindsorContainer(new XmlInterpreter());
container.Register(Component.For>().Named("basicChannelFactoryProvider"));
container.Register(Component.For>().Named("myWcfProxy")
.DependsOn((Dependency.OnComponent(typeof(IChannelFactoryProvider), "basicChannelFactoryProvider"))));
container.Register(Component.For().Interceptors(InterceptorReference.ForKey("myWcfProxy")).First);
container.Register(Component.For());
var myProxy = container.Resolve();
Настройки BasicAuthChannelFactoryProvider при этом можно вынести в конфигурационный файл:
myEndpoint
user
pass
...
В других DI контейнерах можно добиться аналогичного результата с использованием метода CreateInterfaceProxyWithoutTarget класса ProxyGenerator библиотеки Castle.DynamicProxy.