Введение
Я уже писал о том, как сделать 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 — это агрегат и содержит всю необходимую информацию о желании.
public class PresentRequest { public Address Address { get; set; } public Guid Id { get; set; } public PresentRequestStatus Status { get; set; } public string Wish { get; set; } }
public class Address { public string Country { get; set; } public string Recipient { get; set; } public string StreetAddress { get; set; } public int ZipCode { get; set; } }
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);
Плюсы:
- Читабельный URL, например, SantaClaus.org/requests?country=sheldonopolis&status=pending.
Перед перечислением недостатков давайте взглянем на метод 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:
[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 .
В нашем случае, мы имеем два ключа:
- Тип запроса
- Данные запроса, поля объекта
Я вижу следующий алгоритм сериализации:
- Определить тип запроса
- Сериализовать объект запроса в JSON
- Закодировать 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. Реализация очень проста:
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.
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 сервиса мы использовали этот контракт:
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 может быть примерно таким:
[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 сервиса
Я не стану приводить здесь весь код, т.к. он уже приводился ранее. Ниже приводится пример, как Создавать, Обновлять, Получать и Удалять запросы.
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 операции:

Теперь нам необходимо связать запросы с подходящими CRUD-операциями.
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 имеет только методы конфигурирования и обработки.
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.
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:
[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 для получения строки запроса и создания конкретного запроса. Также он может создавать ответное сообщение на основе запроса.
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.
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 запросы на подарки:
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:

Конец
Подход, основанный на сообщениях — это мега мощный архитектурный стиль. Он может помочь создать RESTful сервис со стабильным, обслуживаемым интерфейсом и конечно Санта сам будет доволен получить именно такой RESTful сервис в качестве подарка на Рождество.
Исходники можно скачать со статьи-оригинала или с сайта проекта.
Доступен также nuget package.
Интересная статья по теме: Advantages of message based web services.
ссылка на оригинал статьи http://habrahabr.ru/post/218149/
Добавить комментарий