WCF очень нравится мне как фрэймворк, упрощающий создание коммуникационного слоя. Но WCF's design style меня не устраивает. Я думаю, что создание нового метода для каждого DTO — это не самое хорошее решение, поэтому попытался решить эту проблему.
WCF имеет некоторые ограничения:
Я думаю, что подход в стиле RPC (Remote Procedure Call) не самый подходящий. Сервис должен быть повторно используемым, а влияние бизнес-требований на него должно быть минимальным. Я думаю, что удаленное API должно соответствовать следующим требованиям:
Веб-сервис, основанный на сообщениях, преодолевает большинство ограничений WCF путем добавления абстракции сообщения.
После прочтения статьи вы узнаете, как строить повторно используемые SOAP веб-сервисы, основанные на сообщениях (и перестанете постоянно плодить новые).
Давайте взглянем на подход в стиле RPC, а также на подход, основанный на сообщениях (Message based).
Главная идея стиля RPC — это дать клиентам возможность работать с удаленными сервисами как с локальными объектами. В WCF ServiceContract определяет операции, доступные на стороне клиента. Например:
Контракт сервиса очень прост и содержит три операции. Мы должны изменять клиент после любого изменения в контракте сервиса (например, добавления или удаления операции, изменения сигнатуры операции). Реальное приложение может иметь более чем 10 операций, поэтому сопровождение сервиса и клиентов является очень трудоемким.
В основе подхода, основанного на сообщениях, лежат паттерны Data Transfer Object и Gateway. DTO содержит все необходимые для коммуникации данные, а Gateway изолирует приложение от процесса коммуникации. Так сервис основанный на сообщениях получает сообщение-запрос и возвращает сообщение-ответ. Рассмотрим пример от API Amazon.
Пример запроса:
Пример ответа:
Таким образом, контракт сервиса должен выглядеть примерно так:
Как вы уже знаете, для веб-сервиса, основанного на сообщениях, мы можем использовать объекты
Следующий контракт сервиса описывает коммуникацию с объектом
Ниже представлен весь код метода
Ниже представлены методы
Как вы уже заметили, все CRUD операции имеют асинхронные версии.
SOAP сервис должен уметь:
Наша цель — создать что-то такое, что будет вызывать подходящий CRUD-метод для конкретного
Наибольший интерес представляют интерфейсы

Теперь необходимо связать
Теперь мы готовы для последнего шага: вызова целевого метода. Вот наш SOAP-сервис:
Прежде всего давайте посмотрим на диаграмму последовательности, описывающую процесс выполнения на стороне сервиса.

Давайте погрузимся в код шаг за шагом.
Самое интересное происходит здесь:
SoapRequestMetadata — это главный объект, который соединяет в себе тип операции CRUD, данные запроса (Request), его тип, а также может отвечать на запрос.
А в конце мы просто вызываем соответствующую CRUD-операцию через
Прежде всего, объявим data contracts:
Конфигурационный файл самый обычный:
WCF-сервис чрезвычайно прост:
Привязка всех запросов к обработчикам. Для простоты я создал только один обработчик запросов. Вы можите сделать столько запросов, сколько захотите. Советую почитать статью Мартина Фаулера о CQRS. Это поможет вам сделать правильный выбор. Код связи запросов и обработчиков:
И, наконец,
Код клиента прост:
Результаты выполнения:
клиент:

сервис:

Я надеюсь, что вам понравилось. Здесь вы можете узнать, как строить RESTful веб-сервисы на WCF и Nelibur. Спасибо, что прочли статью (перевод). Исходники можно скачать со страницы оригинала или с GitHub.
WCF имеет некоторые ограничения:
- Не поддерживает перегрузку методов.
- Не имеет универсального API.
- Service Contract зависит от бизнес-требований.
- Версионность должна выполняться на уровне DataContract и методов, имя операции должно быть универсальным.
- Другие не .NET клиенты должны создавать столько клиентов, сколько сервисов у вас есть.
Я думаю, что подход в стиле RPC (Remote Procedure Call) не самый подходящий. Сервис должен быть повторно используемым, а влияние бизнес-требований на него должно быть минимальным. Я думаю, что удаленное API должно соответствовать следующим требованиям:
- Обладать стабильным и универсальным интерфейсом.
- Передавать данные в соответствии с паттерном DTO.
Веб-сервис, основанный на сообщениях, преодолевает большинство ограничений WCF путем добавления абстракции сообщения.
После прочтения статьи вы узнаете, как строить повторно используемые SOAP веб-сервисы, основанные на сообщениях (и перестанете постоянно плодить новые).
Дизайн веб-сервиса
Давайте взглянем на подход в стиле RPC, а также на подход, основанный на сообщениях (Message based).
Дизайн RPC
Главная идея стиля RPC — это дать клиентам возможность работать с удаленными сервисами как с локальными объектами. В WCF ServiceContract определяет операции, доступные на стороне клиента. Например:
[ServiceContract] public interface IRpcService { [OperationContract] void RegisterClient(Client client); [OperationContract] Client GetClientByName(string clientName); [OperationContract] List<Client> GetAllClients(); }
Контракт сервиса очень прост и содержит три операции. Мы должны изменять клиент после любого изменения в контракте сервиса (например, добавления или удаления операции, изменения сигнатуры операции). Реальное приложение может иметь более чем 10 операций, поэтому сопровождение сервиса и клиентов является очень трудоемким.
Дизайн, основанный на сообщениях
В основе подхода, основанного на сообщениях, лежат паттерны Data Transfer Object и Gateway. DTO содержит все необходимые для коммуникации данные, а Gateway изолирует приложение от процесса коммуникации. Так сервис основанный на сообщениях получает сообщение-запрос и возвращает сообщение-ответ. Рассмотрим пример от API Amazon.
Пример запроса:
https://ec2.amazonaws.com/?Action=AllocateAddress Domain=vpc &AUTHPARAMS
Пример ответа:
<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/"> <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> <publicIp>198.51.100.1</publicIp> <domain>vpc</domain> <allocationId>eipalloc-5723d13e</allocationId> </AllocateAddressResponse>
Таким образом, контракт сервиса должен выглядеть примерно так:
гдеpublic interface IMessageBasedService { Response Execute(Request request); }
Request и Response могут быть любыми DTO, то есть одним методом мы можем заменить любой RPC контракт сервиса, но WCF использует стиль RPC.Стиль, основанный на сообщениях
Как вы уже знаете, для веб-сервиса, основанного на сообщениях, мы можем использовать объекты
Request и Response для передачи любого DTO. Но WCF не поддерживает такой дизайн. Все внутренности коммуникаций в WCF основаны на использовании класса Message. То есть WCF конвертирует любой DTO в экземпляр Message и отправляет Message от клиента серверу. Поэтому мы должны использовать класс Message для объектов Request и Response.Следующий контракт сервиса описывает коммуникацию с объектом
Response и без него.[ServiceContract] public interface ISoapService { [OperationContract(Action = ServiceMetadata.Action.ProcessOneWay)] void ProcessOneWay(Message message); [OperationContract(Action = ServiceMetadata.Action.Process, ReplyAction = ServiceMetadata.Action.ProcessResponse)] Message Process(Message message); }
ISoapService позволяет нам передавать любые данные, но этого не достаточно. Мы хотим создавать, удалять объекты и выполнять методы на нем. Что касается меня, лучший выбор — это CRUD-операции на объекте, так мы можем реализовать любую операцию. Прежде всего, давайте создадим SoapServiceClient, который сможет отправлять и получать любой DTO.Soap service client
SoapServiceClient покажет, как создать Message из любого DTO. SoapServiceClient — это враппер, который конвертирует любой DTO в Message и отправляет его сервису. Отправляемое сообщение содержит следующие данные:- DTO
- Тип DTO, необходимый для десериализации на стороне сервера
- Метод, который будет вызван на стороне сервера.
var client = new SoapServiceClient("NeliburSoapService"); ClientResponse response = client.Post<ClientResponse>(createRequest); response = client.Put<ClientResponse>(updateRequest);
Ниже представлен весь код метода
Post класса SoapServiceClient.Обратите, пожалуйста, внимание на методpublic TResponse Post<TResponse>(object request) { return Send<TResponse>(request, OperationTypeHeader.Post); } private TResponse Send<TResponse>(object request, MessageHeader operationType) { using (var factory = new ChannelFactory<ISoapService>(_endpointConfigurationName)) { MessageVersion messageVersion = factory.Endpoint.Binding.MessageVersion; Message message = CreateMessage(request, operationType, messageVersion); ISoapService channel = factory.CreateChannel(); Message result = channel.Process(message); return result.GetBody<TResponse>(); } } private static Message CreateMessage( object request, MessageHeader actionHeader, MessageVersion messageVersion) { Message message = Message.CreateMessage( messageVersion, ServiceMetadata.Operations.Process, request); var contentTypeHeader = new ContentTypeHeader(request.GetType()); message.Headers.Add(contentTypeHeader); message.Headers.Add(actionHeader); return message; }
CreateMessage и на то, как тип DTO и вызываемый метод добавляются через contentTypeHeader and actionHeader.SoapContentTypeHeader и SoapOperationTypeHeader практически идентичны. The SoapContentTypeHeader используется для передачи типа DTO, а SoapOperationTypeHeader — для передачи целевой операции. Меньше слов, больше кода:internal sealed class SoapContentTypeHeader : MessageHeader { private const string NameValue = "nelibur-content-type"; private const string NamespaceValue = "http://nelibur.org/" + NameValue; private readonly string _contentType; public SoapContentTypeHeader(Type contentType) { _contentType = contentType.Name; } public override string Name { get { return NameValue; } } public override string Namespace { get { return NamespaceValue; } } public static string ReadHeader(Message request) { int headerPosition = request.Headers.FindHeader(NameValue, NamespaceValue); if (headerPosition == -1) { return null; } var content = request.Headers.GetHeader<string>(headerPosition); return content; } protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion) { writer.WriteString(_contentType); } }
Ниже представлены методы
SoapServiceClient:public static TResponse Get<TResponse>(object request) public static Task<TResponse> GetAsync<TResponse>(object request) public static void Post(object request) public static Task PostAsync(object request) public static TResponse Post<TResponse>(object request) public static Task<TResponse> PostAsync<TResponse>(object request) public static void Put(object request) public static Task PutAsync(object request) public static TResponse Put<TResponse>(object request) public static Task<TResponse> PutAsync<TResponse>(object request) public static void Delete(object request) public static Task DeleteAsync(object request)
Как вы уже заметили, все CRUD операции имеют асинхронные версии.
SOAP сервис
SOAP сервис должен уметь:
- Создать конкретный Request из Message
- Вызвать целевой метод на Request
- При необходимости создать и вернуть Message из Response
Наша цель — создать что-то такое, что будет вызывать подходящий CRUD-метод для конкретного
Request. В примере ниже показано, как можно добавлять и получать объект Client (клиента).public sealed class ClientProcessor : IPut<CreateClientRequest>, IGet<GetClientRequest> { private readonly List<Client> _clients = new List<Client>(); public object Get(GetClientRequest request) { Client client = _clients.Single(x => x.Id == request.Id); return new ClientResponse {Id = client.Id, Name = client.Name}; } public object Put(CreateClientRequest request) { var client = new Client { Id = Guid.NewGuid(), Name = request.Name }; _clients.Add(client); return new ClientResponse {Id = client.Id}; } }
Наибольший интерес представляют интерфейсы
IGet и IPost. Они представляют операции CRUD. Взглянем на диаграмму классов:
Теперь необходимо связать
Request с соответствующей операцией CRUD. Самый простой путь — связать Request с обработчиком запросов (request Processor). За эту функциональность отличает NeliburService. Давайте взглянем на него.public abstract class NeliburService { internal static readonly RequestMetadataMap _requests = new RequestMetadataMap(); protected static readonly Configuration _configuration = new Configuration(); private static readonly RequestProcessorMap _requestProcessors = new RequestProcessorMap(); protected static void ProcessOneWay(RequestMetadata requestMetaData) { IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type); processor.ProcessOneWay(requestMetaData); } protected static Message Process(RequestMetadata requestMetaData) { IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type); return processor.Process(requestMetaData); } protected sealed class Configuration : IConfiguration { public void Bind<TRequest, TProcessor>(Func<TProcessor> creator) where TRequest : class where TProcessor : IRequestOperation { if (creator == null) { throw Error.ArgumentNull("creator"); } _requestProcessors.Add<TRequest, TProcessor>(creator); _requests.Add<TRequest>(); } public void Bind<TRequest, TProcessor>() where TRequest : class where TProcessor : IRequestOperation, new() { Bind<TRequest, TProcessor>(() => new TProcessor()); } } }
RequestMetadataMap используется для хранения типа объекта Request, который требуется для создания конкретного Request из Message.internal sealed class RequestMetadataMap { private readonly Dictionary<string, Type> _requestTypes = new Dictionary<string, Type>(); internal void Add<TRequest>() where TRequest : class { Type requestType = typeof(TRequest); _requestTypes[requestType.Name] = requestType; } internal RequestMetadata FromRestMessage(Message message) { UriTemplateMatch templateMatch = WebOperationContext.Current.IncomingRequest.UriTemplateMatch; NameValueCollection queryParams = templateMatch.QueryParameters; string typeName = UrlSerializer.FromQueryParams(queryParams).GetTypeValue(); Type targetType = GetRequestType(typeName); return RequestMetadata.FromRestMessage(message, targetType); } internal RequestMetadata FromSoapMessage(Message message) { string typeName = SoapContentTypeHeader.ReadHeader(message); Type targetType = GetRequestType(typeName); return RequestMetadata.FromSoapMessage(message, targetType); } private Type GetRequestType(string typeName) { Type result; if (_requestTypes.TryGetValue(typeName, out result)) { return result; } string errorMessage = string.Format( "Binding on {0} is absent. Use the Bind method on an appropriate NeliburService", typeName); throw Error.InvalidOperation(errorMessage); } }
RequestProcessorMap cсвязывает тип объекта Request с обработчиком.internal sealed class RequestProcessorMap { private readonly Dictionary<Type, IRequestProcessor> _repository = new Dictionary<Type, IRequestProcessor>(); public void Add<TRequest, TProcessor>(Func<TProcessor> creator) where TRequest : class where TProcessor : IRequestOperation { Type requestType = typeof(TRequest); IRequestProcessor context = new RequestProcessor<TRequest, TProcessor>(creator); _repository[requestType] = context; } public IRequestProcessor Get(Type requestType) { return _repository[requestType]; } }
Теперь мы готовы для последнего шага: вызова целевого метода. Вот наш SOAP-сервис:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public sealed class SoapService : ISoapService { public Message Process(Message message) { return NeliburSoapService.Process(message); } public void ProcessOneWay(Message message) { NeliburSoapService.ProcessOneWay(message); } }
Прежде всего давайте посмотрим на диаграмму последовательности, описывающую процесс выполнения на стороне сервиса.

Давайте погрузимся в код шаг за шагом.
NeliburSoapService просто выполняет другой код, взглянем на него.public sealed class NeliburSoapService : NeliburService { private NeliburSoapService() { } public static IConfiguration Configure(Action<IConfiguration> action) { action(_configuration); return _configuration; } public static Message Process(Message message) { RequestMetadata metadata = _requests.FromSoapMessage(message); return Process(metadata); } public static void ProcessOneWay(Message message) { RequestMetadata metadata = _requests.FromSoapMessage(message); ProcessOneWay(metadata); } }
NeliburSoapService просто декорирует RequestMetadataMap, то есть вызывает соответствующий метод для создания RequestMetadata для SOAP Message.Самое интересное происходит здесь:
RequestMetadata requestMetaData = _requests.FromSoapMessage(message)context.Process(requestMetaData).
SoapRequestMetadata — это главный объект, который соединяет в себе тип операции CRUD, данные запроса (Request), его тип, а также может отвечать на запрос.
internal sealed class SoapRequestMetadata : RequestMetadata { private readonly MessageVersion _messageVersion; private readonly object _request; internal SoapRequestMetadata(Message message, Type targetType) : base(targetType) { _messageVersion = message.Version; _request = CreateRequest(message, targetType); OperationType = SoapOperationTypeHeader.ReadHeader(message); } public override string OperationType { get; protected set; } public override Message CreateResponse(object response) { return Message.CreateMessage(_messageVersion, SoapServiceMetadata.Action.ProcessResponse, response); } public override TRequest GetRequest<TRequest>() { return (TRequest)_request; } private static object CreateRequest(Message message, Type targetType) { using (XmlDictionaryReader reader = message.GetReaderAtBodyContents()) { var serializer = new DataContractSerializer(targetType); return serializer.ReadObject(reader); } } }
А в конце мы просто вызываем соответствующую CRUD-операцию через
RequestProcessor. RequestProcessor использует RequestMetadata для определения операции и вызывает ее, когда возвращает результат классу SoapServiceClient.internal sealed class RequestProcessor<TRequest, TProcessor> : IRequestProcessor where TRequest : class where TProcessor : IRequestOperation { private readonly Func<TProcessor> _creator; public RequestProcessor(Func<TProcessor> creator) { _creator = creator; } public Message Process(RequestMetadata metadata) { switch (metadata.OperationType) { case OperationType.Get: return Get(metadata); case OperationType.Post: return Post(metadata); case OperationType.Put: return Put(metadata); case OperationType.Delete: return Delete(metadata); default: string message = string.Format("Invalid operation type: {0}", metadata.OperationType); throw Error.InvalidOperation(message); } } public void ProcessOneWay(RequestMetadata metadata) { switch (metadata.OperationType) { case OperationType.Get: GetOneWay(metadata); break; case OperationType.Post: PostOneWay(metadata); break; case OperationType.Put: PutOneWay(metadata); break; case OperationType.Delete: DeleteOneWay(metadata); break; default: string message = string.Format("Invalid operation type: {0}", metadata.OperationType); throw Error.InvalidOperation(message); } } private Message Delete(RequestMetadata metadata) { var service = (IDelete<TRequest>)_creator(); var request = metadata.GetRequest<TRequest>(); object result = service.Delete(request); return metadata.CreateResponse(result); } private void DeleteOneWay(RequestMetadata metadata) { var service = (IDeleteOneWay<TRequest>)_creator(); var request = metadata.GetRequest<TRequest>(); service.DeleteOneWay(request); } private Message Get(RequestMetadata metadata) { var service = (IGet<TRequest>)_creator(); var request = metadata.GetRequest<TRequest>(); object result = service.Get(request); return metadata.CreateResponse(result); } private void GetOneWay(RequestMetadata metadata) { var service = (IGetOneWay<TRequest>)_creator(); var request = metadata.GetRequest<TRequest>(); service.GetOneWay(request); } private Message Post(RequestMetadata metadata) { var service = (IPost<TRequest>)_creator(); var request = metadata.GetRequest<TRequest>(); object result = service.Post(request); return metadata.CreateResponse(result); } private void PostOneWay(RequestMetadata metadata) { var service = (IPostOneWay<TRequest>)_creator(); var request = metadata.GetRequest<TRequest>(); service.PostOneWay(request); } private Message Put(RequestMetadata metadata) { var service = (IPut<TRequest>)_creator(); var request = metadata.GetRequest<TRequest>(); object result = service.Put(request); return metadata.CreateResponse(result); } private void PutOneWay(RequestMetadata metadata) { var service = (IPutOneWay<TRequest>)_creator(); var request = metadata.GetRequest<TRequest>(); service.PutOneWay(request); } }
Демонстрационный пример
Прежде всего, объявим data contracts:
CreateClientRequest— запрос на создание нового клиентаUpdateClientRequest— запрос на обновление email клиентаGetClientRequest— запрос на получение клиента по idClientResponse— информация о клиентеRemoveClientRequest— запрос на удаление клиента
Server's side
Конфигурационный файл самый обычный:
<configuration> <!--WCF--> <system.serviceModel> <services> <service name="Nelibur.ServiceModel.Services.Default.SoapServicePerCall"> <endpoint address="http://localhost:5060/service" binding="basicHttpBinding" bindingConfiguration="ServiceBinding" contract="Nelibur.ServiceModel.Contracts.ISoapService" /> </service> </services> <bindings> <basicHttpBinding> <binding name="ServiceBinding"> <security mode="None"> <transport clientCredentialType="None" /> </security> </binding> </basicHttpBinding> </bindings> </system.serviceModel> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> </configuration>
WCF-сервис чрезвычайно прост:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public sealed class SoapServicePerCall : ISoapService { /// <summary> /// Process message with response. /// </summary> /// <param name="message">Request message.</param> /// <returns>Response message.</returns> public Message Process(Message message) { return NeliburSoapService.Process(message); } /// <summary> /// Process message without response. /// </summary> /// <param name="message">Request message.</param> public void ProcessOneWay(Message message) { NeliburSoapService.ProcessOneWay(message); } }
Привязка всех запросов к обработчикам. Для простоты я создал только один обработчик запросов. Вы можите сделать столько запросов, сколько захотите. Советую почитать статью Мартина Фаулера о CQRS. Это поможет вам сделать правильный выбор. Код связи запросов и обработчиков:
private static void BindRequestToProcessors() { NeliburSoapService.Configure(x => { x.Bind<CreateClientRequest, ClientProcessor>(); x.Bind<UpdateClientRequest, ClientProcessor>(); x.Bind<DeleteClientRequest, ClientProcessor>(); x.Bind<GetClientRequest, ClientProcessor>(); }); }
И, наконец,
ClientProcessor:public sealed class ClientProcessor : IPost<CreateClientRequest>, IGet<GetClientRequest>, IDeleteOneWay<DeleteClientRequest>, IPut<UpdateClientRequest> { private static List<Client> _clients = new List<Client>(); public void DeleteOneWay(DeleteClientRequest request) { Console.WriteLine("Delete Request: {0}\n", request); _clients = _clients.Where(x => x.Id != request.Id).ToList(); } public object Get(GetClientRequest request) { Console.WriteLine("Get Request: {0}", request); Client client = _clients.Single(x => x.Id == request.Id); return new ClientResponse { Id = client.Id, Email = client.Email }; } public object Post(CreateClientRequest request) { Console.WriteLine("Post Request: {0}", request); var client = new Client { Id = Guid.NewGuid(), Email = request.Email }; _clients.Add(client); return new ClientResponse { Id = client.Id, Email = client.Email }; } public object Put(UpdateClientRequest request) { Console.WriteLine("Put Request: {0}", request); Client client = _clients.Single(x => x.Id == request.Id); client.Email = request.Email; return new ClientResponse { Id = client.Id, Email = client.Email }; } }
Client's side
Код клиента прост:
private static void Main() { var client = new SoapServiceClient("NeliburSoapService"); var createRequest = new CreateClientRequest { Email = "email@email.com" }; Console.WriteLine("POST Request: {0}", createRequest); ClientResponse response = client.Post<ClientResponse>(createRequest); Console.WriteLine("POST Response: {0}\n", response); var updateRequest = new UpdateClientRequest { Email = "new@email.com", Id = response.Id }; Console.WriteLine("PUT Request: {0}", updateRequest); response = client.Put<ClientResponse>(updateRequest); Console.WriteLine("PUT Response: {0}\n", response); var getClientRequest = new GetClientRequest { Id = response.Id }; Console.WriteLine("GET Request: {0}", getClientRequest); response = client.Get<ClientResponse>(getClientRequest); Console.WriteLine("GET Response: {0}\n", response); var deleteRequest = new DeleteClientRequest { Id = response.Id }; Console.WriteLine("DELETE Request: {0}", deleteRequest); client.Delete(deleteRequest); Console.ReadKey(); }
Результаты выполнения:
клиент:

сервис:

Вот и все
Я надеюсь, что вам понравилось. Здесь вы можете узнать, как строить RESTful веб-сервисы на WCF и Nelibur. Спасибо, что прочли статью (перевод). Исходники можно скачать со страницы оригинала или с GitHub.
