9 April 2014

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

.NET
Translation
Tutorial
Original author: Sergey Morenko

Введение


Я уже писал о том, как сделать 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.
Tags:WCFRESTfulC#Nelibur
Hubs: .NET
+4
31.3k 70
Comments 57
Popular right now
Top of the last 24 hours