- PVSM.RU - https://www.pvsm.ru -
Для desktop-мира wcf остаётся самым распространенным способом организации клиент-серверного взаимодействия в .net как для локальных, так и для глобальных сетей. Он гибок в настройке, прост в использовании и прозрачен.
По крайней мере, так должно быть. На практике добавление нового сервиса — это рутина. Нужно не забыть прописать конфигурацию на сервере, сделать то же самое на клиенте, нужно написать или сгенерировать proxy-класс. Поддерживать конфиги неудобно. Если сервис изменился, то нужно вносить изменения в proxy-класс. А ещё не забыть про регистрации в IoC-контейнере. И добавление новых хостов для новых сервисов. И еще хочется простой асинхронности. По отдельности всё просто, но даже для статьи я дописывал этот список уже трижды, и не уверен, что не упустил чего-нибудь.
Время автоматизировать. Простейший сценарий от создания решения до вызова wcf-сервиса выглядит так:
var assembly = Assembly.GetExecutingAssembly();
_serviceHostManager.StartServices(assembly);
var articles = await _myServiceExecutor.Execute(service => service.GetArticles());
Создадим клиент-серверное приложение. Клиенты передают серверу индекс числа из последовательности Фибоначчи, сервер возвращает число из последовательности с заданным индексом. Из кода в статье убрано логирование и обработка ошибок, но в коде на github [2] я привожу более полный пример для иллюстрации целостного подхода.
Структура проектов, приближенная к реальности:
Server.Contracts содержит интерфейсы wcf-сервисов, Server — их реализацию, а так же реализацию хостера — класса, который будет поднимать wcf-сервисы. BL — логика сервера. ConsoleServiceHost хостит сервисы в домене консольного приложения. Client.Presentaion содержит соответствующий слой клиента. В нашем примере там только команда вызова сервиса и обработка результата. Client — консольное приложение, использующее предыдущую сборку для обработки ввода пользователя.
Собственно, nuget-пакеты нужно устанавливать следующим образом:
В RikropWcfExample.Server.Contracts добавим описание wcf-сервиса [4]:
using System.ServiceModel;
using System.Threading.Tasks;
namespace RikropWcfExample.Server.Contracts
{
[ServiceContract]
public interface ICalculatorService
{
[OperationContract]
Task<ulong> GetFibonacciNumber(int n);
}
}
Реализация в CalculatorService.cs [5] будет передавать запрос и возвращать результат из слоя бизнес-логики:
using RikropWcfExample.Server.BL;
using RikropWcfExample.Server.Contracts;
using System.Threading.Tasks;
namespace RikropWcfExample.Server
{
public class CalculatorService : ICalculatorService
{
private readonly FibonacciCalculator _fibonacciCalculator;
public CalculatorService(FibonacciCalculator.ICtor fibonacciCalculatorCtor)
{
_fibonacciCalculator = fibonacciCalculatorCtor.Create();
}
public async Task<ulong> GetFibonacciNumber(int n)
{
return await _fibonacciCalculator.Calculate(n);
}
}
}
Пока можно заметить одну особенность — wcf-сервис использует async/await для описания асинхронности. В остальном никаких специфических конструкций нет.
Теперь перейдем к регистрации. Простейший синтаксис для сервера указывает тип привязки (NetTcp) список поведений, которые должны быть добавлены к сервисам:
private static IUnityContainer RegisterWcfHosting(this IUnityContainer container, string serviceIp, int servicePort)
{
container
.RegisterServerWcf(
o => o.RegisterServiceConnection(reg => reg.NetTcp(serviceIp, servicePort))
.RegisterServiceHostFactory(reg => reg.WithBehaviors().AddDependencyInjectionBehavior())
);
return container;
}
Для клиента указывается тип обёртки-исполнителя для сервисов (ServiceExecutor), тип обёртки над привязкой (Standart предполагает NetTcp) и, собственно, адрес сервера:
private static IUnityContainer RegisterWcf(this IUnityContainer container, string serviceIp, int servicePort)
{
container
.RegisterClientWcf(o => o.RegisterServiceExecutor(reg => reg.Standard()
.WithExceptionConverters()
.AddFaultToBusinessConverter())
.RegisterChannelWrapperFactory(reg => reg.Standard())
.RegisterServiceConnection(reg => reg.NetTcp(serviceIp, servicePort)));
return container;
}
Всё. Не нужно регистрировать каждый сервис по интерфейсу, не нужно создавать Proxy, не нужно прописывать wcf в конфигурации — эти регистрации позволят сразу начать работать с сервисами так, будто это локальные вызовы.
Но сначала нужно захостить их на сервере. Библиотека Rikrop.Core.Wcf [6] уже включает класс ServiceHostManager [7], который сделает всю работу самостоятельно. Прописывать каждый сервис не нужно:
using Rikrop.Core.Wcf;
using System.Reflection;
namespace RikropWcfExample.Server
{
public class WcfHoster
{
private readonly ServiceHostManager _serviceHostManager;
public WcfHoster(ServiceHostManager serviceHostManager)
{
_serviceHostManager = serviceHostManager;
}
public void Start()
{
var assembly = Assembly.GetExecutingAssembly();
_serviceHostManager.StartServices(assembly);
}
public void Stop()
{
_serviceHostManager.StopServices();
}
}
}
Запустим сервер [8]:
public static void Main()
{
using (var serverContainer = new UnityContainer())
{
serverContainer.RegisterServerDependencies();
var service = serverContainer.Resolve<WcfHoster>();
service.Start();
Console.WriteLine("Сервер запущен. Для остановки нажмите Enter.");
Console.ReadLine();
service.Stop();
}
}
Запустим клиент [9]:
static void Main()
{
using (var container = new UnityContainer())
{
container.RegisterClientDependencies();
var calculateFibonacciCommandCtor = container.Resolve<CalculateFibonacciCommand.ICtor>();
int number;
while (int.TryParse(GetUserInput(), out number))
{
var command = calculateFibonacciCommandCtor.Create();
var result = command.Execute(number);
Console.WriteLine("Fibonacci[{0}] = {1}", number, result);
}
}
}
Работает:
Может показаться, что предложенное решение требует довольно много инфраструктурного кода и не несёт преимуществ перед обычным использованием wcf. Проще всего будет показать разницу на примере типовых ситуаций, возникающих при работе над проектами.
table width=«100%»>
Теперь можно вызвать новый метод на клиенте.
Теперь можно вызвать новый сервис на клиенте.
Rikrop.Core.Wcf(.Unity) | Без использования библиотек |
---|---|
Теперь можно вызвать новый сервис на клиенте. |
Теперь можно вызвать новый сервис на клиенте. |
Rikrop.Core.Wcf(.Unity) | Без использования библиотек |
---|---|
* см. public Result Custom [10]<TServiceConnection>(LifetimeManager lifetimeManager = null, params InjectionMember[] injectionMembers) where TServiceConnection: IServiceConnection |
* Количество работы пропорционально количеству wcf-сервисов. Если их 100, то остаётся только надеяться, что быстрая замена по файлу сработает. |
Rikrop.Core.Wcf(.Unity) | Без использования библиотек |
---|---|
|
|
Стоит сказать несколько слов о последних двух пунктах. При правильной организации app.config вносить изменения в него довольно легко. Это можно делать без пересборки приложения. В реальной разработке структурированная конфигурация wcf попадается довольно редко, чему виной итеративность разработки. Изменять конфигурацию непрогаммисту тоже приходится нечасто, если начальные настройки удовлетворяют требованиям. При этом, легко совершить опечатку, которую компилятор не найдёт.
Расширение функциональности и изменение поведения происходит за счёт добавления при регистрации Behavior. Наиболее частым в применении является поведение, отвечающее за передачу в заголовке wcf-сообщения информации о сессии.
Для демонстрации функционала был создан отдельный branch [11] с расширенным кодом предыдущего примера. В стандартной настройке поведения разработчику предлагается выбрать метод авторизации — это OperationContract, который будет доступен пользователям без сессии в заголовке сообщения. Вызов остальных методов будет возможен только при заполненном заголовке.
Регистрация на сервере будет выглядеть следующим образом:
container
.RegisterType<ISessionResolver<Session>, SessionResolver<Session>>()
.RegisterServerWcf(
o => o.RegisterServiceConnection(reg => reg.NetTcp(serviceIp, servicePort))
.RegisterServiceHostFactory(reg => reg.WithBehaviors()
.AddErrorHandlersBehavior(eReg => eReg.AddBusinessErrorHandler().AddLoggingErrorHandler(NLogger.CreateEventLogTarget()))
.AddDependencyInjectionBehavior()
.AddServiceAuthorizationBehavior(sReg => sReg.WithStandardAuthorizationManager()
.WithStandardSessionHeaderInfo("ExampleNamespace", "SessionId")
.WithOperationContextSessionIdInitializer()
.WithSessionAuthStrategy<Session>()
.WithLoginMethod<ILoginService>(s => s.Login())
.WithOperationContextSessionIdResolver()
.WithInMemorySessionRepository()
.WithStandardSessionCopier())
)
);
Можно изменить способ авторизации, добавив свою имплементацию System.ServiceModel.ServiceAuthorizationManager [12], изменить способ инициализации идентификатора сессии, метод проверки авторизации, способ извлечения сессии из контекста выполнения запроса, способ хранения и копирования сессий на сервере. В обобщенном случае регистрация AuthorizationBehavior может выглядеть следующим образом:
.AddServiceAuthorizationBehavior(sReg => sReg.WithCustomAuthorizationManager<ServiceAuthorizationManagerImpl>()
.WithCustomSessionHeaderInfo<ISessionHeaderInfoImpl>()
.WithCustomSessionIdInitializer<ISessionIdInitializerImpl>()
.WithCustomAuthStrategy<IAuthStrategyImpl>()
.WithLoginMethod<ILoginService>(s => s.Login())
.WithCustomSessionIdResolver<ISessionIdResolverImpl>()
.WithCustomSessionRepository<ISessionRepositoryImpl<MySessionImpl>>()
.WithCustomSessionCopier<ISessionCopierImpl<MySessionImpl>>())
Клиентская регистрация так же меняется:
private static IUnityContainer RegisterWcf(this IUnityContainer container, string serviceIp, int servicePort)
{
container
.RegisterType<ClientSession>(new ContainerControlledLifetimeManager())
.RegisterClientWcf(o => o
.RegisterServiceExecutor(reg => reg.Standard()
.WithExceptionConverters()
.AddFaultToBusinessConverter())
.RegisterChannelWrapperFactory(reg => reg.Standard())
.RegisterServiceConnection(reg => reg
.NetTcp(serviceIp, servicePort)
.WithBehaviors()
.AddSessionBehavior(sReg => sReg
.WithStandardSessionHeaderInfo("ExampleNamespace", "SessionId")
.WithCustomSessionIdResolver<ClientSession>(new ContainerControlledLifetimeManager())
.WithStandardMessageInspectorFactory<ILoginService>(service => service.Login()))));
return container;
}
Результат:
var newSession = Session.Create(userId);
_sessionRepository.Add(newSession);
return new SessionDto { SessionId = newSession.SessionId, Username = "ExampleUserName" };
var clientSession = container.Resolve<ClientSession>();
var sessionDto = Task.Run(async () => await loginServiceExecutor.Execute(s => s.Login())).Result;
clientSession.Session = sessionDto;
public async Task<ulong> GetFibonacciNumber(int n)
{
var session = _sessionResolver.GetSession();
_logger.LogInfo(
string.Format("User with SessionId={0} and UserId={1} called CalculatorService.GetFibonacciNumber", session.SessionId, session.UserId));
return await _fibonacciCalculator.Calculate(n);
}
_logger.LogInfo(string.Format("SessionId {0} with name {1} begin calculate Fibomacci", _clientSession.SessionId, _clientSession.Session.Username));
Большую часть инфраструктуры предоставляет библиотека System.ServiceModel.dll. Однако, есть несколько решений, которые нужно рассмотреть подробнее.
Основой взаимодействия между клиентом и сервером служат реализации интерфейса IServiceExecutor [16], находящиеся в библиотеке Rikrop.Core.Wcf.
public interface IServiceExecutor<out TService>
{
Task Execute(Func<TService, Task> action);
Task<TResult> Execute<TResult>(Func<TService, Task<TResult>> func);
}
В простейшем случае [17] открывается канал и метод вызывается в контексте этого канала:
public async Task<TResult> Execute<TResult>(Func<TService, Task<TResult>> func)
{
using (var wrapper = _channelWrapperFactory.CreateWrapper())
{
return await func(wrapper.Channel);
}
}
Более сложные реализации могут конвертировать ошибки [18] или дополнительно извещать об окончании обработки изменением свойства. Наибольшее распространение эти идеи получили в WPF-реализациях IServiceExecutor, где с помощью ServiceExecutorFactory [19] можно создать обёртки над wcf-сервисом, позволяющие использовать DataBinding для оповещения UI о продолжительной операции, или отображающие popup с произвольной информацией во время ожидания ответа от сервера.
Для легкой реализации главную роль играют Fluent interface при регистрации и стандартные реализации инфраструктуры библиотеки, из-за чего даже даже в самых сложных конструкциях легко разобраться с первого раза с помощью подскзок студии:
В статье так же косвенно упомянаются другие библиотеки:
private static IUnityContainer RegisterFactories(this IUnityContainer container)
{
new[] { Assembly.GetExecutingAssembly(), typeof (FibonacciCalculator).Assembly }
.SelectMany(assembly => assembly.DefinedTypes.Where(type => type.Name == "ICtor"))
.Where(type => !container.IsRegistered(type))
.ForEach(container.RegisterFactory);
return container;
}
private static IUnityContainer RegisterLogger(this IUnityContainer container)
{
container.RegisterType<ILogger>(new ContainerControlledLifetimeManager(),
new InjectionFactory(f => NLogger.CreateConsoleTarget()));
return container;
}
Единожды настроив инфраструктуру на проекте, можно надолго забыть о сетевой природе взаимодействия через IServiceExexutor. Лучше всего применять системный подход и использовать так же бибилиотки для построения настольных приложений с применением mvvm-паттерна, взаимодействия с БД [22], логирования и других типовых задач. Но даже при нежелании использовать незнакомый и не всегда привычный фреймворк, можно найти применение идеям, лежащим в его основе. Расширяемость компонент, строгая типизация при конфигурировании, прозрачность взаимодействия на всех слоях, минимизация инфраструктурного кода и затрат времени на поддержание инфрастурктуры — это то, о чём важно не забывать при написании калькулятора и многопользовательской Enterprise-системы. Можно скачать код библиотек и подключить их к решению проектом вместо использования библиотеки. Это позволит изучить работу под отладчиком и при необходимости внести свои изменения.
Нет ничего лучше практики. Я узнал, что у нас был опыт перевода довольно крупного проекта (~300.000 строк кода) в стадии где-то между разработкой и поддержкой на использование Rikrop.Core.Wcf. Это довольно интересный опыт мучений с async/await в .net 4.0, кастомизации работы с сессиями, извлечения настроек из конфига и перевод их в c#-форму. Если это кому-нибудь будет интересно, можно описать конкретный пример перехода на эту библиотеку без пеетягивания всего фреймворка.
Еще есть решение для wpf с информированием пользователя через блокировку ui или всплывающие окна, реализованные через ServiceExecutorFactory. Это частный пример и он относится куда больше к wpf, чем к wcf. Но это может дать больше информации о преимуществах библиотеки и мотивации к использованию.
Автор: Vadimyan
Источник [23]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/79225
Ссылки в тексте:
[1] Rikrop.Core.Wcf.Unity: https://www.nuget.org/packages/Rikrop.Core.Wcf.Unity/
[2] github: https://github.com/Vadimyan/Rikrop.WcfExample
[3] хостинга: https://www.reg.ru/?rlink=reflink-717
[4] wcf-сервиса: https://github.com/Vadimyan/Rikrop.WcfExample/blob/master/RikropWcfExample.Server.Contracts/ICalculatorService.cs
[5] CalculatorService.cs: https://github.com/Vadimyan/Rikrop.WcfExample/blob/94b5e343d90bd3755d16eefa5fe87c57a8a552fc/RikropWcfExample.Server/CalculatorService.cs
[6] Rikrop.Core.Wcf: https://github.com/rikrop/Rikrop.Core.Wcf
[7] ServiceHostManager: https://github.com/rikrop/Rikrop.Core.Wcf/blob/985035ecca80182cd2895d7188cf4272c90d8240/Rikrop.Core.Wcf/ServiceHostManager.cs
[8] сервер: https://github.com/Vadimyan/Rikrop.WcfExample/blob/master/RikropWcfExample.Server.ConsoleServiceHost/Program.cs
[9] клиент: https://github.com/Vadimyan/Rikrop.WcfExample/blob/master/RikropWcfExample.Client/Program.cs
[10] Custom: https://github.com/rikrop/Rikrop.Core.Wcf.Unity/blob/master/Rikrop.Core.Wcf.Unity/ClientRegistration/ServiceConnectionRegistrator.cs
[11] branch: https://github.com/Vadimyan/Rikrop.WcfExample/tree/AuthorizationExample
[12] System.ServiceModel.ServiceAuthorizationManager: http://msdn.microsoft.com/ru-ru/library/system.servicemodel.serviceauthorizationmanager(v=vs.110).aspx
[13] wcf-контракте: https://github.com/Vadimyan/Rikrop.WcfExample/blob/AuthorizationExample/RikropWcfExample.Server/LoginService.cs
[14] Клиент: https://github.com/Vadimyan/Rikrop.WcfExample/blob/AuthorizationExample/RikropWcfExample.Client/Program.cs
[15] получить данные: https://github.com/Vadimyan/Rikrop.WcfExample/blob/AuthorizationExample/RikropWcfExample.Client.Presentation/CalculateFibonacciCommand.cs
[16] IServiceExecutor: https://github.com/rikrop/Rikrop.Core.Framework/blob/master/Rikrop.Core.Framework/Services/IServiceExecutor.cs
[17] простейшем случае: https://github.com/rikrop/Rikrop.Core.Wcf/blob/master/Rikrop.Core.Wcf/ServiceExecutor.cs
[18] конвертировать ошибки: https://github.com/rikrop/Rikrop.Core.Wcf/blob/master/Rikrop.Core.Wcf/ServiceExecutorWithExceptionConversion.cs
[19] ServiceExecutorFactory: https://github.com/rikrop/Rikrop.Core.Wpf/blob/master/Rikrop.Core.Wpf/Async/ServiceExecutorFactory.cs
[20] автофабрик: https://github.com/rikrop/Rikrop.Core.Framework.Unity/blob/master/Rikrop.Core.Framework.Unity/Factories/FactoryExtension.cs
[21] логгерами: https://github.com/rikrop/Rikrop.Core.Logging.NLog
[22] взаимодействия с БД: http://habrahabr.ru/post/243353/
[23] Источник: http://habrahabr.ru/post/246961/
Нажмите здесь для печати.