[Перевод] Построение SOAP веб-сервисов, основанных на сообщениях, с помощью WCF
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
https://ec2.amazonaws.com/? Action=AllocateAddress
Domain=vpc
&AUTHPARAMS
Пример ответа:
[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
response = client.Put
private TResponse Send
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
protected override void OnWriteHeaderContents (XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteString (_contentType);
}
}
Ниже представлены методы SoapServiceClient:
public static TResponse Get
public static Task
public static void Post (object request)
public static Task PostAsync (object request)
public static TResponse Post
public static Task
public static void Put (object request)
public static Task PutAsync (object request)
public static TResponse Put
public static Task
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
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
public void Bind
internal void Add
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
public void Add
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
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.Самое интересное происходит здесь: 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
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
public RequestProcessor (Func
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
private void DeleteOneWay (RequestMetadata metadata)
{
var service = (IDeleteOneWay
private Message Get (RequestMetadata metadata)
{
var service = (IGet
private void GetOneWay (RequestMetadata metadata)
{
var service = (IGetOneWay
private Message Post (RequestMetadata metadata)
{
var service = (IPost
private void PostOneWay (RequestMetadata metadata)
{
var service = (IPostOneWay
private Message Put (RequestMetadata metadata)
{
var service = (IPut
private void PutOneWay (RequestMetadata metadata)
{
var service = (IPutOneWay
[ServiceBehavior (InstanceContextMode = InstanceContextMode.PerCall)]
public sealed class SoapServicePerCall: ISoapService
{
///
///
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
var updateRequest = new UpdateClientRequest { Email = «new@email.com», Id = response.Id };
Console.WriteLine («PUT Request: {0}», updateRequest);
response = client.Put
var getClientRequest = new GetClientRequest
{
Id = response.Id
};
Console.WriteLine («GET Request: {0}», getClientRequest);
response = client.Get
var deleteRequest = new DeleteClientRequest { Id = response.Id }; Console.WriteLine («DELETE Request: {0}», deleteRequest); client.Delete (deleteRequest);
Console.ReadKey (); } Результаты выполнения: клиент:
сервис:
Вот и все Я надеюсь, что вам понравилось. Здесь вы можете узнать, как строить RESTful веб-сервисы на WCF и Nelibur. Спасибо, что прочли статью (перевод). Исходники можно скачать со страницы оригинала или с GitHub.