[Из песочницы] Внедрение 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.

© Habrahabr.ru