Monday, October 26, 2009

Common Service Factory.

Common Service Factory – библиотека, позволяющая использовать Dependency Inversion Principle для WCF- сервисов. Основным ее отличием от всех остальных (использующих реализацию определенных IOC-контейнеров) является возможность работы с любым контейнером, т.к. она построена на основе другого полезного проекта Common Service Locator, отсюда сразу следует уточнение, что для используемого вами контейнера должен быть реализован адаптер к локатору служб, однако это не является проблемой, потому что для большинства популярных контейнеров они уже реализованы и их реализация является тривиальной задачей.

У автора достаточно четко описан способ работы с библиотекой, я же хочу показать немного иной способ, используя конкретный пример и контейнер. И так контейнер Castle Windsor и сервис:

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
[ServiceContract(SessionMode = SessionMode.Allowed)]
public interface IMyService
{
[OperationContract]
int DoWork();
}
}

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class MyService : IMyService, INotMyService
{
private int balance;
private readonly IBalanceChangingPolicy policy;

public MyService(IBalanceChangingPolicy policy)
{
this.policy = policy;
balance = 0;
}

public int DoWork()
{
balance = policy.Apply(balance);
return balance;
}

public void DoNotWork()
{
}
}
}

Простой сервис как-то изменяющий абстрактный баланс и возвращающий новое значение. Политика изменения баланса по-умолчанию, тоже выглядит довольно просто.

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
public interface IBalanceChangingPolicy
{
int Apply(int balance);
}
}


namespace Govorov.Nikita.Blog.CommonServiceFactory
{
public class DefaultBalanceChangingPolicy : IBalanceChangingPolicy
{
public int Apply(int balance)
{
return ++balance;
}
}
}

Теперь необходимо наш сервис где-то и как-то расположить, для простоты и оригинальности пусть это будет консольное приложение.

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
class Program
{
static void Main()
{
var locator = new WindsorServiceLocator( new WindsorContainer()
.Register(Component.For<IMyService>()
.ImplementedBy<MyService>().LifeStyle.Transient)
.Register(Component.For<IBalanceChangingPolicy>()
.ImplementedBy<DefaultBalanceChangingPolicy>())
);
ServiceLocator.SetLocatorProvider(() => locator);
var host = new ServiceHost(typeof(MyService));
host.Open();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
host.Close();
}
}
}

Код, приведенный выше, безнадежно прост, единственное что можно отметить: класс ServiceHost это нестандартный класс из System.ServiceModel, по идее это класс из библиотеки Common Service Factory, однако для простоты его использования, я сделал так.

using CommonServiceFactoryServiceHost = CommonServiceFactory.ServiceHost;

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
public class ServiceHost : CommonServiceFactoryServiceHost
{
public ServiceHost(Type serviceType, params Uri[] baseAddresses)
: base(serviceType, baseAddresses)
{
}
}
}

Ну и ожидаемо-стандартная конфигурация.

<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="Govorov.Nikita.Blog.CommonServiceFactory.MyServiceBehavior">
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceMetadata policyVersion="Policy15"/>
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="Govorov.Nikita.Blog.CommonServiceFactory.MyServiceBehavior" name="Govorov.Nikita.Blog.CommonServiceFactory.MyService">
<endpoint address="" binding="netTcpBinding" contract="Govorov.Nikita.Blog.CommonServiceFactory.IMyService" />
<endpoint address="mex" binding="mexTcpBinding" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:9998"/>
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>

Запустив сервис, сгенерируем клиентский код и  напишем вызовы сервиса.

namespace Govorov.Nikita.Blog.CommonServiceFactory.Client
{
internal class Program
{
private static void Main()
{
ServiceClientScope<IMyService>.Use(service =>
{
Console.WriteLine(service.DoWork());
Console.WriteLine(service.DoWork());
Console.WriteLine(service.DoWork());
});

Console.ReadKey();
}
}
}

ServiceClientScope – статический класс, позволяющий обойти неприятность допущенную WCF-разработчиками в методе Dispose канала.

namespace Govorov.Nikita.Blog.CommonServiceFactory.Client
{
public static class ServiceClientScope<T>
{
private static readonly ChannelFactory<T> ChannelFactory = new ChannelFactory<T>("*");

public static void Use(Action<T> codeBlock)
{
var proxy = (IClientChannel) ChannelFactory.CreateChannel();
bool success = false;
try
{
codeBlock((T) proxy);
proxy.Close();
success = true;
}
finally
{
if (!success)
{
proxy.Abort();
}
}
}
}
}

Запустив клиентское приложение мы получим три цифры 1 на консоль, теперь можно попробовать создать класс:

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
public class NonDefaultBalanceChangingPolicy : IBalanceChangingPolicy
{
public int Apply(int balance)
{
return (balance + 2);
}
}

изменить код  bootstrapper’а:

var locator = new WindsorServiceLocator(new WindsorContainer()
.Register(Component.For<IMyService>()
.ImplementedBy<MyService>().LifeStyle.Transient)
.Register(Component.For<IBalanceChangingPolicy>()
.ImplementedBy<NonDefaultBalanceChangingPolicy>())
);

и снова запустить оба приложения, получим три цифры 2 на консоль, если изменить InstanceContextMode на PerSession получим 2,4,6.

Из типовых сценариев использования так же присутствует возможность использования конвенций для выбора интерфейса, по которому будет выбран объект из контейнера(т.е. из контейнера можно выбирать объект сервиса не по контракту(по-умолчанию используется I+ServiceType), а по любому другому интерфейсу). Не смотря на то что  это уже добавляет немного запутанности, так же присутствуют дополнительные опасные места:

  1. Cледует обращать внимание на используемый InstanceContextMode и тип жизни вашего сервиса в IOC-контейнере(LifeStyle для Castle Windsor, и т.п. для др.). Например, не получится использовать сервис в режиме InstanceContextMode.Single т.к.  для таких сервисов  в WCF зашит вызов конструктора по-умолчанию(и если у вашего сервиса есть конструктор по-умолчанию, даже приватный, сервис будет создан с помощью этого конструктора без каких-либо инъекций). Если вам необходимо использовать сервис в режиме InstanceContextMode.Single, возможен вариант с использованием конструктора ServiceHost(object singletonInstance, params System.Uri[] baseAddresses) класса ServiceHost из System.ServiceModel, куда передается уже сконфигурированный объект из контейнера, так же можно оставить InstanceContextMode по-умолчанию(PerSession) и определить объект в контейнере как singleton.
  2. Не следует передавать в ServiceHost  один тип сервиса (new ServiceHost(typeof(MyService))), а регистрировать в контейнере другой(Register(Component.For<IMyService>().ImplementedBy<MyService2>().LifeStyle.Transient), это может  приводить к ошибкам, т.к. WCF зарегистрирует для вашего сервиса данные из атрибутов(например, ServiceBehavior) именно от того типа который был передан в конструктор ServiceHost’а.

Ну и в заключении, для использования библиотеки под IIS(WPAS), в разметке *.svc файла  необходимо указать следующую директиву, например:

<%@ ServiceHost Language="C#" Debug="true" Service="WebApplication.MyService, WebApplication" CodeBehind="MyService.svc.cs" Factory="CommonServiceFactory.ServiceHostFactory, CommonServiceFactory" %>