WCF очень нравится мне как фрэймворк, упрощающий создание коммуникационного слоя. Но WCF's design style меня не устраивает. Я думаю, что создание нового метода для каждого DTO — это не самое хорошее решение, поэтому попытался решить эту проблему.
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, необходимый для десериализации на стороне сервера
- Метод, который будет вызван на стороне сервера.
Наша цель — создать повторно используемый клиент для SOAP веб-сервиса, который сможет отправлять/получать любой запрос/ответ и выполнять любые операции над объектом. Как упоминалось ранее — лучше всего для этого подходит CRUD, поэтому клиент может выглядеть примерно так:
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.
Автор: Truba