Построение RESTful Message Based веб-сервисов на WCF

от автора

Введение

Я уже писал о том, как сделать SOAP Message Based веб-сервис на WCF. А сейчас хочу рассказать о проектировании и построении RESTful Message Based веб-сервисов на WCF. Для понимания данной статьи необходимы базовые знания о REST и о том, как создавать RESTful веб-сервисы на WCF. Для ознакомления с RESTful веб-сервисами вы можете ознакомиться с: A Guide to Designing and Building RESTful Web Services with WCF 3.5.

В статье я постараюсь раскрыть и решить проблемы дизайна RESTful. Вы узнаете, как построить RESTful веб-сервис, который:

  • Обладает стабильным и универсальным интерфейсом.
  • Передает данные в соответствие с паттерном DTO.


Давайте спроектируем WCF веб сервис для Санты Клауса. Санта очень любит архитектурный стиль REST и совсем не любит Open Data Protocol (OData), поэтому он выдвинул следующие требования:

  • Сервис должен иметь RESTful API
  • Сервис должен обладать следующим функционалом:
    • Сохранение запроса на подарок.
    • Обновление запроса на подарок.
    • Получение запроса на подарокпо Статусу и Стране.
    • Удаление запроса на подарокпо Id.

Определение основных бизнес-объектов

Наша цель — спроектировать веб-сервис в стиле RESTful, поэтому давайте оставим бизнес-объекты простыми на столько, на сколько это возможно.

Рассмотрим класс Запрос на подарок (далее PresentRequest). PresentRequest — это агрегат и содержит всю необходимую информацию о желании.

PresentRequest

public class PresentRequest {     public Address Address { get; set; }     public Guid Id { get; set; }     public PresentRequestStatus Status { get; set; }     public string Wish { get; set; } }  

Address

public class Address {     public string Country { get; set; }     public string Recipient { get; set; }     public string StreetAddress { get; set; }     public int ZipCode { get; set; } }   

PresentRequestStatus

public enum PresentRequestStatus {     Pending,     Accepted,     Rejected,     Completed }  

Теперь мы имеем все необходимое для начала.

RESTful веб-сервис на WCF: проблема проектирования

На этом шаге мы определим интерфейс веб-сервиса. Давайте начнем с метода Save.

Сохранение PresentRequest

Простейшая реализация будет выглядеть так:

public void Save(PresentRequest request)  

Клиент заполняет все поля и отправляет запрос на веб-сервис. Метод Save возвращает void, т.к. мы знаем, что сервис будет высоконагруженным, поэтому генерация уникального Id ложится на плечи клиента.

В соответствие со стилем проектирования RESTful, мы должны декорировать метод Save атрибутом WebInvoke и указать подходящий HTTP-метод. Вот маленькая шпаргалка по HTTP методам:

Operation HTTP
Create PUT / POST
Read GET
Update PUT / PATCH
Delete DELETE

В результате получаем такой ServiceContract:

[ServiceContract] public interface IPresentRequestService {     [WebInvoke(Method = "POST", UriTemplate = "requests")]     [OperationContract]     void Save(PresentRequest request); }  

Замечание: ServiceContract — это основная часть сервиса, которая должна обладать стабильностью и гибкостью. Все клиенты зависят от ServiceContract, поэтому мы должны быть очень аккуратными с какими-либо изменениями в контракте.

Метод Save имеет как плюсы, так и минусы.
Плюсы:

  • Метод абстрактный, поэтому мы можем легко добавлять поля в PresentRequest
  • Запрос отправляется как объект, а не как параметры URL

Большинство разработчиков знают из книги «Мифический человеко-месяц» о том, что первая версия ПО будет выброшена. То же самое относится и к ServiceContract, поэтому мы должны постараться сделать его гибким на столько, на сколько это возможно.
Минусы:

  • Мы должны иметь столько же методов Save, сколько разных объектов-наследников PresentRequest у нас будет. Но как насчет ООП?

Я знаю о KnownTypeAttribute, но нам прийдется создать бесполезную иерархию классов только для процесса десериализации.

Операции Create, Update и Delete имеют аналогичные плюсы и минусы. Операция Get — отличается и явзяется, имхо, самым трудным в сопровождении методом.

Получение PresentRequests

Для операции Get параметры отправляются в строке запроса. В нашем случае, для получения PresentRequest по статусу и стране, нам нужно создать что-то вроде

[WebGet(UriTemplate = "requests?country={country}&status={status}")] [OperationContract] List<PresentRequest> Get(string country, string status); 

Плюсы:

Перед перечислением недостатков давайте взглянем на метод Get. Представим, что мы используем этот метод внутри нашего приложения, без WCF.

public interface IPresentRequestService {     List<PresentRequest> Get(string country, string status); }  

Одним из самых больших проблем этого метода — сигнатура. Мы должны будем обновлять реализацию сервиса после любых изменений в сигнатуре метода. Этот метод — хрупкий и имеет запашок. Таким образом, операция Get в стиле RESTful является трудно сопровождаемой по умолчанию.
Вот более удачное решение, мы можем менять запрос без изменения интерфейса:

public interface IPresentRequestService {     List<PresentRequest> Get(PresentRequestQuery query); } 

Все необходимые данные запроса содержит класс PresentRequestQuery:

public class PresentRequestQuery {     public string Country { get; set; }     public string Status { get; set; } }  

Минусы:
Как было сказано выше, метод Get имеет хрупкую сигнатуру, поэтому расширить функциональность без breaking changes действительно сложно. Параметры операции Get отправляются как строка запроса с простыми полями, которые также представлены в сигнатуре метода Get. Связность между параметрами отсутствует, т.к. WCF не создает объект запроса на основе параметров.
Давайте взглянем на пример: URL SantaClaus.org/requests?country=sheldonopolis&status=pending для получения PresentReuqests по стране и статусу.
Вот соответствующий метод в WCF-сервисе:

public List<PresentRequest> Get(string country, string status) {     throw new NotImplementedException(); }  

Согласно сигнатуре метода связность между country и status отсутствуют. Фактически, мы не знаем, что означает country и status, мы можем лишь предполагать. По моему мнению, WCF должно уметь создать сроку запроса на основе объекта запроса (сериализовать), а также создать объект запроса на основе строки запроса (десериализация). Таким образом, для отправки следующий объект запроса:

public class PresentRequestQuery {     public string Country { get; set; }     public string Status { get; set; } } 

должен быть сериализован в country=sheldonopolis&status=pending, а после получения строка запроса должна быть десериализована в экземпляр PresentRequestQuery и метод Get должен выглядеть так:

public List<PresentRequest> Get(PresentRequestQuery query) {     throw new NotImplementedException(); }  

Мы должны создать столько методов Get, сколько запросов мы имеем. Вот пример кода из WCF’s Guide to Designing and Building RESTful Web Services:

BookmarkService

[ServiceContract] public partial class BookmarkService {     [WebGet(UriTemplate = "?tag={tag}")]     [OperationContract]     Bookmarks GetPublicBookmarks(string tag) {...}          [WebGet(UriTemplate = "{username}?tag={tag}")]     [OperationContract]     Bookmarks GetUserPublicBookmarks(string username, string tag) {...}          [WebGet(UriTemplate = "users/{username}/bookmarks?tag={tag}")]     [OperationContract]     Bookmarks GetUserBookmarks(string username, string tag) {...}          [WebGet(UriTemplate = "users/{username}/profile")]     [OperationContract]     UserProfile GetUserProfile(string username) {...}          [WebGet(UriTemplate = "users/{username}")]     [OperationContract]     User GetUser(string username) {...}          [WebGet(UriTemplate = "users/{username}/bookmarks/{bookmark_id}")]     [OperationContract]     Bookmark GetBookmark(string username, string bookmark_id) {...}     ... }  

Я не понимаю, почему WCF не поддерживает сериализацию строки запроса, то есть создание объекта из строки запроса. Этот простой трюк мог бы помочь создать более стабильную сигнатуру метода. С другой стороны, метод Get может иметь такую сигнатуру. Так вид метода повторно является повторно используемым и полиморфным.

Message Get (Message request); 

Минусы операции Get:

  • Методы трудно сопровождаемы
  • Необходимо создавать слишком много методов Get
  • Отсутствует связность между параметрами запроса
  • Полиморфизм отсутствует

Пожалуйста, имейте ввиду, что WCF SOAP сервис имеет полиморфизм, точнее имеет специальный полиморфизм (ad hoc polymorphism), реализуемый через KnownTypeAttribute, но, по-моему, WCF должен поддерживать параметрический полиморфизм.

Заключение

WCF как RESTful фрэймворк имеет несколько архитектурных особенностей, которые усложняет создание повторно используемых и стабильных сервисов. С другой стороны, WCF имеет все необходимое для решения этих проблем.

RESTful Web Service на WCF: улучшенный дизайн

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

Сериализация и десериализация URL

Мы уже видели класс PresentRequestQuery, но теперь давайте сериализуем его.

public class PresentRequestQuery {     public string Country { get; set; }     public string Status { get; set; } } 

Как нам известно, Get отправляет параметры в виде строки запроса, поэтому наш метод сериализации должен создавать валидную строку запроса. Идеальная строка запроса, полученная в результате сериализации, должна выглядеть так: country=sheldonopolis&status=pending и мы хотим создать что-то похожее. Идеальный результат сериализации имеет один недостаток: отсутствие связи между параметрами, поэтому мы не можем десериализовать URL в объект запроса. Наш механизм сериализации должен решить и эту проблему.

Вообще говоря, строка запроса — это коллекция различных пар «ключ-значение»: key1=value1&key2=value2&key3=value3 .
В нашем случае, мы имеем два ключа:

  • Тип запроса
  • Данные запроса, поля объекта

Я вижу следующий алгоритм сериализации:

  1. Определить тип запроса
  2. Сериализовать объект запроса в JSON
  3. Закодировать JSON

Результирующая строка запроса должна соответствовать маске: type={request type}&data={request data}
Вот экземпляр объекта запроса:

var query = new PresentRequestQuery {     Country = "sheldonopolis",     Status = "pending" }; 

Результирующая строка запроса: type=PresentRequestQuery&data=%7B%22Country%22%3A%22sheldonopolis%22%2C%22Status%22%3A%22pending%22%7D
Эта строка запроса может быть легко десериализована в экземпляр PresentRequestQuery. Реализация очень проста:

CreateQueryParams<T>(T value)

private static NameValueCollection CreateQueryParams<T>(T value) {     string data = JsonDataSerializer.ToString(value);     var result = new NameValueCollection         {             { RestServiceMetadata.ParamName.Type, UrlEncode(typeof(T).Name) },             { RestServiceMetadata.ParamName.Data, UrlEncode(data) }         };     return result; }  

, где UrlEncode вызывает лишь Uri.EscapeDataString и JsonDataContractSerializer — это экземпляр DataContractJsonSerializer.

ToString<T>(T value)

public static string ToString<T>(T value) {     using (var stream = new MemoryStream())     {         var serializer = new DataContractJsonSerializer(typeof(T));         serializer.WriteObject(stream, value);         return Encoding.UTF8.GetString(stream.ToArray());     } }  

Теперь мы готовы к следующему шагу — использованию подхода, основанного на сообщениях. Для SOAP сервиса мы использовали этот контракт:

ISoapService

SeriviceContract:

[ServiceContract] public interface ISoapService {     [OperationContract(Action = ServiceMetadata.Action.Process)]     void Process(Message message);       [OperationContract(Action = ServiceMetadata.Action.ProcessWithResponse,         ReplyAction = ServiceMetadata.Action.ProcessResponse)]     Message ProcessWithResponse(Message message); }  

Стиль RESTful требует наличия как минимум четырех методов: Get, Post, Put, Delete and ServiceContract может быть примерно таким:

IJsonService

[ServiceContract] public interface IJsonService {     [OperationContract]     [WebInvoke(Method = OperationType.Delete,         UriTemplate = RestServiceMetadata.Path.Delete,         RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]     void Delete(Message message);       [OperationContract]     [WebInvoke(Method = OperationType.Delete,         UriTemplate = RestServiceMetadata.Path.DeleteWithResponse,         RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]     Message DeleteWithResponse(Message message);       [OperationContract]     [WebGet(UriTemplate = RestServiceMetadata.Path.Get,         RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]     void Get(Message message);       [OperationContract]     [WebGet(UriTemplate = RestServiceMetadata.Path.GetWithResponse,         RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]     Message GetWithResponse(Message message);       [OperationContract]     [WebInvoke(Method = OperationType.Post,         UriTemplate = RestServiceMetadata.Path.Post,         RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]     void Post(Message message);       [OperationContract]     [WebInvoke(Method = OperationType.Post,         UriTemplate = RestServiceMetadata.Path.PostWithResponse,         RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]     Message PostWithResponse(Message message);       [OperationContract]     [WebInvoke(Method = OperationType.Put,         UriTemplate = RestServiceMetadata.Path.Put,         RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]     void Put(Message message);       [OperationContract]     [WebInvoke(Method = OperationType.Put,         UriTemplate = RestServiceMetadata.Path.PutWithResponse,         RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]     Message PutWithResponse(Message message); }  

IJsonService обладает гибкостью, стабильностью и легкостью сопровождения. Мы можем передавать любые данные, так как сервис зависит только от класса Message, который является фундаментальным для WCF(MSDN). Еще одно преимущество — это CRUD. Используя IJsonService и сериализацию в URL мы можем создавать повторно используемые RESTful сервисы с параметрическим полиморфизмом.

Реализация RESTful сервиса

Я не стану приводить здесь весь код, т.к. он уже приводился ранее. Ниже приводится пример, как Создавать, Обновлять, Получать и Удалять запросы.

ClientProcessor

public sealed class ClientProcessor : IPostWithResponse<CreateClientRequest>,                                       IGetWithResponse<GetClientRequest>,                                       IDelete<DeleteClientRequest>,                                       IPutWithResponse<UpdateClientRequest> {     private static List<Client> _clients = new List<Client>();       public void Delete(DeleteClientRequest request)     {         _clients = _clients.Where(x => x.Id != request.Id).ToList();     }       public object GetWithResponse(GetClientRequest request)     {         Client client = _clients.Single(x => x.Id == request.Id);         return new ClientResponse { Id = client.Id, Email = client.Email };     }       public object PostWithResponse(CreateClientRequest 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 PutWithResponse(UpdateClientRequest request)     {         Client client = _clients.Single(x => x.Id == request.Id);         client.Email = request.Email;         return new ClientResponse { Id = client.Id, Email = client.Email };     } }  

Следующие интерфейсы представляют CRUD операции:
image
Теперь нам необходимо связать запросы с подходящими CRUD-операциями.

ServiceProcessor

public abstract class ServiceProcessor {     internal static readonly RequestMetadataMap _requests = new RequestMetadataMap();     protected static readonly Configuration _configuration = new Configuration();     private static readonly RequestProcessorMap _requestProcessors = new RequestProcessorMap();      protected static void Process(RequestMetadata requestMetaData)     {         IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type);         processor.Process(requestMetaData);     }      protected static Message ProcessWithResponse(RequestMetadata requestMetaData)     {         IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type);         return processor.ProcessWithResponse(requestMetaData);     }      protected sealed class Configuration : IConfiguration     {         public void Bind<TRequest, TProcessor>(Func<TProcessor> creator)             where TRequest : class             where TProcessor : IRequestOperation         {             if (creator == null)             {                 throw new ArgumentNullException("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());         }     } } 

Конкретный ServiceProcessor имеет только методы конфигурирования и обработки.

RestServiceProcessor

public sealed class RestServiceProcessor : ServiceProcessor {     private RestServiceProcessor()     {     }      public static IConfiguration Configure(Action<IConfiguration> action)     {         action(_configuration);         return _configuration;     }      public static void Process(Message message)     {         RequestMetadata metadata = _requests.FromRestMessage(message);         Process(metadata);     }      public static Message ProcessWithResponse(Message message)     {         RequestMetadata metadata = _requests.FromRestMessage(message);         return ProcessWithResponse(metadata);     } } 

RequestMetadataMap используется для хранения типов запросов, которые необходимы для создания конкретных запросов из экземпляров Message.

RequestMetadataMap

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 ServiceProcessor", typeName);         throw new InvalidOperationException(errorMessage);     } } 

Посмотрим на повторно используемую реализацию IJsonService:

JsonServicePerCall

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public sealed class JsonServicePerCall : IJsonService {     public void Delete(Message message)     {         RestServiceProcessor.Process(message);     }       public Message DeleteWithResponse(Message message)     {         return RestServiceProcessor.ProcessWithResponse(message);     }       public void Get(Message message)     {         RestServiceProcessor.Process(message);     }       public Message GetWithResponse(Message message)     {         return RestServiceProcessor.ProcessWithResponse(message);     }       public void Post(Message message)     {         RestServiceProcessor.Process(message);     }       public Message PostWithResponse(Message message)     {         return RestServiceProcessor.ProcessWithResponse(message);     }       public void Put(Message message)     {         RestServiceProcessor.Process(message);     }       public Message PutWithResponse(Message message)     {         return RestServiceProcessor.ProcessWithResponse(message);     } } 

Как видите, можно отправлять все, что угодно и полностью в соответствии с RESTful.
Самое интересное происходит в RestRequestMetadata, классе, который помогает создать конкретный запрос из URL. Перед тем, как взглянуть на реализацию RestRequestMetadata, я хочу дать некоторые пояснения. RestRequestMetadata использует WebOperationContext для получения строки запроса и создания конкретного запроса. Также он может создавать ответное сообщение на основе запроса.

RestRequestMetadata

internal sealed class RestRequestMetadata : RequestMetadata {     private readonly object _request;     private readonly WebOperationContext _webOperationContext;       internal RestRequestMetadata(Message message, Type targetType) : base(targetType)     {         _webOperationContext = WebOperationContext.Current;         OperationType = GetOperationType(message);         _request = CreateRequest(message, targetType);     }       public override string OperationType { get; protected set; }       public override Message CreateResponse(object response)     {         var serializer = new DataContractJsonSerializer(response.GetType());         return _webOperationContext.CreateJsonResponse(response, serializer);     }       public override TRequest GetRequest<TRequest>()     {         return (TRequest)_request;     }       private static object CreateRequestFromContent(Message message, Type targetType)     {         using (var stream = new MemoryStream())         {             XmlDictionaryWriter writer = JsonReaderWriterFactory.CreateJsonWriter(stream);             message.WriteMessage(writer);             writer.Flush();             var serializer = new DataContractJsonSerializer(targetType);             stream.Position = 0;             return serializer.ReadObject(stream);         }     }       private static string GetOperationType(Message message)     {         var httpReq = (HttpRequestMessageProperty)message.Properties[HttpRequestMessageProperty.Name];         return httpReq.Method;     }       private object CraeteRequestFromUrl(Type targetType)     {         UriTemplateMatch templateMatch = _webOperationContext.IncomingRequest.UriTemplateMatch;         NameValueCollection queryParams = templateMatch.QueryParameters;         return UrlSerializer.FromQueryParams(queryParams).GetRequestValue(targetType);     }       private object CreateRequest(Message message, Type targetType)     {         if (IsRequestByUrl())         {             return CraeteRequestFromUrl(targetType);         }           return CreateRequestFromContent(message, targetType);     }       private bool IsRequestByUrl()     {         return OperationType == Operations.OperationType.Get ||             OperationType == Operations.OperationType.Delete;     } } 

Все конкретные запросы обрабатываются классом RequestProcessor.

RequestProcessor<TRequest, TProcessor>

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 void Process(RequestMetadata metadata)     {         switch (metadata.OperationType)         {             case OperationType.Get:                 Get(metadata);                 break;             case OperationType.Post:                 Post(metadata);                 break;             case OperationType.Put:                 Put(metadata);                 break;             case OperationType.Delete:                 Delete(metadata);                 break;             default:                 string message = string.Format("Invalid operation type: {0}", metadata.OperationType);                 throw new InvalidOperationException(message);         }     }       public Message ProcessWithResponse(RequestMetadata metadata)     {         switch (metadata.OperationType)         {             case OperationType.Get:                 return GetWithResponse(metadata);             case OperationType.Post:                 return PostWithResponse(metadata);             case OperationType.Put:                 return PutWithResponse(metadata);             case OperationType.Delete:                 return DeleteWithResponse(metadata);             default:                 string message = string.Format("Invalid operation type: {0}", metadata.OperationType);                 throw new InvalidOperationException(message);         }     }       private void Delete(RequestMetadata metadata)     {         var service = (IDelete<TRequest>)_creator();         var request = metadata.GetRequest<TRequest>();         service.Delete(request);     }       private Message DeleteWithResponse(RequestMetadata metadata)     {         var service = (IDeleteWithResponse<TRequest>)_creator();         var request = metadata.GetRequest<TRequest>();         object result = service.DeleteWithResponse(request);         return metadata.CreateResponse(result);     }       private void Get(RequestMetadata metadata)     {         var service = (IGet<TRequest>)_creator();         var request = metadata.GetRequest<TRequest>();         service.Get(request);     }       private Message GetWithResponse(RequestMetadata metadata)     {         var service = (IGetWithResponse<TRequest>)_creator();         var request = metadata.GetRequest<TRequest>();         object result = service.GetWithResponse(request);         return metadata.CreateResponse(result);     }       private void Post(RequestMetadata metadata)     {         var service = (IPost<TRequest>)_creator();         var request = metadata.GetRequest<TRequest>();         service.Post(request);     }       private Message PostWithResponse(RequestMetadata metadata)     {         var service = (IPostWithResponse<TRequest>)_creator();         var request = metadata.GetRequest<TRequest>();         object result = service.PostWithResponse(request);         return metadata.CreateResponse(result);     }       private void Put(RequestMetadata metadata)     {         var service = (IPut<TRequest>)_creator();         var request = metadata.GetRequest<TRequest>();         service.Put(request);     }       private Message PutWithResponse(RequestMetadata metadata)     {         var service = (IPutWithResponse<TRequest>)_creator();         var request = metadata.GetRequest<TRequest>();         object result = service.PutWithResponse(request);         return metadata.CreateResponse(result);     } } 

Клиент RESTful сервиса

Клиент достаточно прост, просто сериализует данные в строку запроса и отправляет сервису. Клиент основан на HttpClient. Ниже приведены методы клиента:

Методы клиента

public void Delete<TRequest>(TRequest request)     where TRequest : class   public TResponse Delete<TRequest, TResponse>(TRequest request)     where TRequest : class   public Task DeleteAsync<TRequest>(TRequest request)     where TRequest : class   public Task<TResponse> DeleteAsync<TRequest, TResponse>(TRequest request)     where TRequest : class   public void Get<TRequest>(TRequest request)     where TRequest : class   public TResponse Get<TRequest, TResponse>(TRequest request)     where TRequest : class   public Task GetAsync<TRequest>(TRequest request)     where TRequest : class   public Task<TResponse> GetAsync<TRequest, TResponse>(TRequest request)     where TRequest : class   public void Post<TRequest>(TRequest request)     where TRequest : class   public TResponse Post<TRequest, TResponse>(TRequest request)     where TRequest : class   public Task<TResponse> PostAsync<TRequest, TResponse>(TRequest request)     where TRequest : class   public Task PostAsync<TRequest>(TRequest request)     where TRequest : class   public void Put<TRequest>(TRequest request)     where TRequest : class   public TResponse Put<TRequest, TResponse>(TRequest request)     where TRequest : class   public Task PutAsync<TRequest>(TRequest request)     where TRequest : class   public Task<TResponse> PutAsync<TRequest, TResponse>(TRequest request)     where TRequest : class 

А теперь давайте сделаем Санту счастливым обладателем RESTful — сервиса, основанного на сообщениях.

Пример RESTful сервиса

Санта до сих пор ожидает RESTful сервиса, способного сохранять и искать запросы на подарки по фильтру.

Сервис

Файл конфигурации самый обычный:

Конфигурация

<?xml version="1.0" encoding="utf-8"?>   <configuration>       <system.serviceModel>         <services>             <service name="Nelibur.ServiceModel.Services.JsonServicePerCall">                 <host>                     <baseAddresses>                         <add baseAddress="http://localhost:9090/requests" />                     </baseAddresses>                 </host>                 <endpoint binding="webHttpBinding"                           contract="Nelibur.ServiceModel.Contracts.IJsonService" />             </service>         </services>     </system.serviceModel>       <startup>         <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />     </startup> </configuration> 

JsonServicePerCall и IJsonService уже упоминались выше.

Ниже представдена привязка и другие настройки. Биндинг говорит, что PresentRequestProcessor будет обрабатывать PresentRequest и PresentRequestQuery.

Настройка привязки

private static void Main() {     RestServiceProcessor.Configure(x =>     {         x.Bind<PresentRequest, PresentRequestProcessor>();         x.Bind<PresentRequestQuery, PresentRequestProcessor>();         x.Bind<UpdatePresentRequestStatus, PresentRequestProcessor>();         x.Bind<DeletePresentRequestsByStatus, PresentRequestProcessor>();     });      using (var serviceHost = new WebServiceHost(typeof(JsonServicePerCall)))     {         serviceHost.Open();          Console.WriteLine("Santa Clause Service has started");         Console.ReadKey();          serviceHost.Close();     } } 

И наконец, PresentRequestProcessor показывает как Get, Post, Put and Delete запросы на подарки:

PresentRequestProcessor

public sealed class PresentRequestProcessor : IPost<PresentRequest>,                                               IPost<UpdatePresentRequestStatus>,                                               IGetWithResponse<PresentRequestQuery>,                                               IDelete<DeletePresentRequestsByStatus> {     private static List<PresentRequest> _requests = new List<PresentRequest>();      public void Delete(DeletePresentRequestsByStatus request)     {         var status = (PresentRequestStatus)Enum.Parse(typeof(PresentRequestStatus), request.Status);         _requests = _requests.Where(x => x.Status != status).ToList();         Console.WriteLine("Request list was updated, current count: {0}", _requests.Count);     }      public object GetWithResponse(PresentRequestQuery request)     {         Console.WriteLine("Get Present Requests by: {0}", request);         var status = (PresentRequestStatus)Enum.Parse(typeof(PresentRequestStatus), request.Status);         return _requests.Where(x => x.Status == status)                         .Where(x => x.Address.Country == request.Country)                         .ToList();     }      public void Post(PresentRequest request)     {         request.Status = PresentRequestStatus.Pending;         _requests.Add(request);         Console.WriteLine("Request was added, Id: {0}", request.Id);     }      public void Post(UpdatePresentRequestStatus request)     {         Console.WriteLine("Update requests on status: {0}", request.Status);         var status = (PresentRequestStatus)Enum.Parse(typeof(PresentRequestStatus), request.Status);         _requests.ForEach(x => x.Status = status);     } } 

Клиент

Код клиента самодокументируемый:

Клиент

private static void Main() {     var client = new JsonServiceClient("http://localhost:9090/requests");      var presentRequest = new PresentRequest         {             Id = Guid.NewGuid(),             Address = new Address                 {                     Country = "sheldonopolis",                 },             Wish = "Could you please help developers to understand, " +                    "WCF is awesome only with Nelibur"         };     client.Post(presentRequest);      var requestQuery = new PresentRequestQuery         {             Country = "sheldonopolis",             Status = PresentRequestStatus.Pending.ToString()         };     List<PresentRequest> pendingRequests = client.Get<PresentRequestQuery, List<PresentRequest>>(requestQuery);     Console.WriteLine("Pending present requests count: {0}", pendingRequests.Count);      var updatePresentRequestStatus = new UpdatePresentRequestStatus         {             Status = PresentRequestStatus.Accepted.ToString()         };     client.Post(updatePresentRequestStatus);      var deleteByStatus = new DeletePresentRequestsByStatus         {             Status = PresentRequestStatus.Accepted.ToString()         };     client.Delete(deleteByStatus);      Console.WriteLine("Press any key for Exit");     Console.ReadKey(); } 

Результаты выполнения: скриншот программы Fiddler:
image

Конец

Подход, основанный на сообщениях — это мега мощный архитектурный стиль. Он может помочь создать RESTful сервис со стабильным, обслуживаемым интерфейсом и конечно Санта сам будет доволен получить именно такой RESTful сервис в качестве подарка на Рождество.

Исходники можно скачать со статьи-оригинала или с сайта проекта.
Доступен также nuget package.

Интересная статья по теме: Advantages of message based web services.

ссылка на оригинал статьи http://habrahabr.ru/post/218149/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *