Введение
Я уже писал о том, как сделать 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]
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 для получения
PresentReuqest
s по стране и статусу.Вот соответствующий метод в 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
.В нашем случае, мы имеем два ключа:
- Тип запроса
- Данные запроса, поля объекта
Я вижу следующий алгоритм сериализации:
- Определить тип запроса
- Сериализовать объект запроса в 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
. Реализация очень проста: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 операции:

Теперь нам необходимо связать запросы с подходящими 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:

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