Многим разработчикам время от времени приходится взаимодействовать с различными 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<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.

Комментарии (4)


  1. Vadimyan
    30.11.2015 15:51

    Всё равно приходится писать бойлерплейт, регистрировать в контейнере Proxy, не забывать о вспомогательных классах. А ведь можно обойтись совсем без этого:

    container
            .RegisterClientWcf(o => o.RegisterServiceExecutor(reg => reg.Standard()
                                                     .WithExceptionConverters()
                                                     .AddFaultToBusinessConverter())
            .RegisterChannelWrapperFactory(reg => reg.Standard())
            .RegisterServiceConnection(reg => reg.NetTcp(serviceIp, servicePort)));
    container.Resolve<ISomeService>();
    
    .


    1. mihalagutin
      30.11.2015 17:03

      Спасибо, почитаю.

      Приведенные класс WcfProxyInterceptor и интерфейс IChannelFactoryProvider не претендуют на полноценную библиотеку. Право создать таковую для своих нужд с возможностью более лаконичной регистрации компонентов в любимом DI контейнере оставляется заинтересованным читателям.

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


      1. Vadimyan
        30.11.2015 18:13

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

        Да, согласен, в приведенной статье есть лок на Unity, но только потому, что код конфигурации вынесен для удобства в отдельную библиотеку. Здесь можно поступить аналогичным образом, давая коннекторы к разным IoC-контейнерам. По крайней мере, дать хелпер для написания вместо:

        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>());
        

        примерно этого (простите, не силён в Castle):
        container.RegisterWcfFor<ISomeService>().WithCannelFactory("basicChannelFactoryProvider").WithCustomProxy("myWcfProxy");
        

        И спрятать внутри эти DependsOn ForKey. Понятно, что подобные хелперы для себя делают не только лишь все, но иметь его в готовом виде приятнее.
        В любом случае, за статью спасибо.


        1. mihalagutin
          30.11.2015 19:56

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

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