WCF очень нравится мне как фрэймворк, упрощающий создание коммуникационного слоя. Но WCF's design style меня не устраивает. Я думаю, что создание нового метода для каждого DTO — это не самое хорошее решение, поэтому попытался решить эту проблему.
WCF имеет некоторые ограничения:
Я думаю, что подход в стиле RPC (Remote Procedure Call) не самый подходящий. Сервис должен быть повторно используемым, а влияние бизнес-требований на него должно быть минимальным. Я думаю, что удаленное API должно соответствовать следующим требованиям:
Веб-сервис, основанный на сообщениях, преодолевает большинство ограничений WCF путем добавления абстракции сообщения.
После прочтения статьи вы узнаете, как строить повторно используемые SOAP веб-сервисы, основанные на сообщениях (и перестанете постоянно плодить новые).
Давайте взглянем на подход в стиле RPC, а также на подход, основанный на сообщениях (Message based).
Главная идея стиля RPC — это дать клиентам возможность работать с удаленными сервисами как с локальными объектами. В WCF ServiceContract определяет операции, доступные на стороне клиента. Например:
Контракт сервиса очень прост и содержит три операции. Мы должны изменять клиент после любого изменения в контракте сервиса (например, добавления или удаления операции, изменения сигнатуры операции). Реальное приложение может иметь более чем 10 операций, поэтому сопровождение сервиса и клиентов является очень трудоемким.
В основе подхода, основанного на сообщениях, лежат паттерны Data Transfer Object и Gateway. DTO содержит все необходимые для коммуникации данные, а Gateway изолирует приложение от процесса коммуникации. Так сервис основанный на сообщениях получает сообщение-запрос и возвращает сообщение-ответ. Рассмотрим пример от API Amazon.
Пример запроса:
Пример ответа:
Таким образом, контракт сервиса должен выглядеть примерно так:
Как вы уже знаете, для веб-сервиса, основанного на сообщениях, мы можем использовать объекты
Следующий контракт сервиса описывает коммуникацию с объектом
Ниже представлен весь код метода
Ниже представлены методы
Как вы уже заметили, все CRUD операции имеют асинхронные версии.
SOAP сервис должен уметь:
Наша цель — создать что-то такое, что будет вызывать подходящий CRUD-метод для конкретного
Наибольший интерес представляют интерфейсы
Теперь необходимо связать
Теперь мы готовы для последнего шага: вызова целевого метода. Вот наш SOAP-сервис:
Прежде всего давайте посмотрим на диаграмму последовательности, описывающую процесс выполнения на стороне сервиса.
Давайте погрузимся в код шаг за шагом.
Самое интересное происходит здесь:
SoapRequestMetadata — это главный объект, который соединяет в себе тип операции CRUD, данные запроса (Request), его тип, а также может отвечать на запрос.
А в конце мы просто вызываем соответствующую CRUD-операцию через
Прежде всего, объявим data contracts:
Конфигурационный файл самый обычный:
WCF-сервис чрезвычайно прост:
Привязка всех запросов к обработчикам. Для простоты я создал только один обработчик запросов. Вы можите сделать столько запросов, сколько захотите. Советую почитать статью Мартина Фаулера о CQRS. Это поможет вам сделать правильный выбор. Код связи запросов и обработчиков:
И, наконец,
Код клиента прост:
Результаты выполнения:
клиент:
сервис:
Я надеюсь, что вам понравилось. Здесь вы можете узнать, как строить RESTful веб-сервисы на WCF и Nelibur. Спасибо, что прочли статью (перевод). Исходники можно скачать со страницы оригинала или с GitHub.
WCF имеет некоторые ограничения:
- Не поддерживает перегрузку методов.
- Не имеет универсального API.
- Service Contract зависит от бизнес-требований.
- Версионность должна выполняться на уровне DataContract и методов, имя операции должно быть универсальным.
- Другие не .NET клиенты должны создавать столько клиентов, сколько сервисов у вас есть.
Я думаю, что подход в стиле RPC (Remote Procedure Call) не самый подходящий. Сервис должен быть повторно используемым, а влияние бизнес-требований на него должно быть минимальным. Я думаю, что удаленное API должно соответствовать следующим требованиям:
- Обладать стабильным и универсальным интерфейсом.
- Передавать данные в соответствии с паттерном DTO.
Веб-сервис, основанный на сообщениях, преодолевает большинство ограничений WCF путем добавления абстракции сообщения.
После прочтения статьи вы узнаете, как строить повторно используемые SOAP веб-сервисы, основанные на сообщениях (и перестанете постоянно плодить новые).
Дизайн веб-сервиса
Давайте взглянем на подход в стиле RPC, а также на подход, основанный на сообщениях (Message based).
Дизайн RPC
Главная идея стиля RPC — это дать клиентам возможность работать с удаленными сервисами как с локальными объектами. В WCF ServiceContract определяет операции, доступные на стороне клиента. Например:
[ServiceContract]
public interface IRpcService
{
[OperationContract]
void RegisterClient(Client client);
[OperationContract]
Client GetClientByName(string clientName);
[OperationContract]
List<Client> GetAllClients();
}
Контракт сервиса очень прост и содержит три операции. Мы должны изменять клиент после любого изменения в контракте сервиса (например, добавления или удаления операции, изменения сигнатуры операции). Реальное приложение может иметь более чем 10 операций, поэтому сопровождение сервиса и клиентов является очень трудоемким.
Дизайн, основанный на сообщениях
В основе подхода, основанного на сообщениях, лежат паттерны Data Transfer Object и Gateway. DTO содержит все необходимые для коммуникации данные, а Gateway изолирует приложение от процесса коммуникации. Так сервис основанный на сообщениях получает сообщение-запрос и возвращает сообщение-ответ. Рассмотрим пример от API Amazon.
Пример запроса:
https://ec2.amazonaws.com/?Action=AllocateAddress
Domain=vpc
&AUTHPARAMS
Пример ответа:
<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<publicIp>198.51.100.1</publicIp>
<domain>vpc</domain>
<allocationId>eipalloc-5723d13e</allocationId>
</AllocateAddressResponse>
Таким образом, контракт сервиса должен выглядеть примерно так:
public interface IMessageBasedService
{
Response Execute(Request request);
}
где Request
и Response
могут быть любыми DTO, то есть одним методом мы можем заменить любой RPC контракт сервиса, но WCF использует стиль RPC.Стиль, основанный на сообщениях
Как вы уже знаете, для веб-сервиса, основанного на сообщениях, мы можем использовать объекты
Request
и Response
для передачи любого DTO. Но WCF не поддерживает такой дизайн. Все внутренности коммуникаций в WCF основаны на использовании класса Message. То есть WCF конвертирует любой DTO в экземпляр Message
и отправляет Message
от клиента серверу. Поэтому мы должны использовать класс Message
для объектов Request
и Response
.Следующий контракт сервиса описывает коммуникацию с объектом
Response
и без него.[ServiceContract]
public interface ISoapService
{
[OperationContract(Action = ServiceMetadata.Action.ProcessOneWay)]
void ProcessOneWay(Message message);
[OperationContract(Action = ServiceMetadata.Action.Process,
ReplyAction = ServiceMetadata.Action.ProcessResponse)]
Message Process(Message message);
}
ISoapService
позволяет нам передавать любые данные, но этого не достаточно. Мы хотим создавать, удалять объекты и выполнять методы на нем. Что касается меня, лучший выбор — это CRUD-операции на объекте, так мы можем реализовать любую операцию. Прежде всего, давайте создадим SoapServiceClient
, который сможет отправлять и получать любой DTO.Soap service client
SoapServiceClient
покажет, как создать Message
из любого DTO. SoapServiceClient
— это враппер, который конвертирует любой DTO в Message
и отправляет его сервису. Отправляемое сообщение содержит следующие данные:- DTO
- Тип DTO, необходимый для десериализации на стороне сервера
- Метод, который будет вызван на стороне сервера.
var client = new SoapServiceClient("NeliburSoapService");
ClientResponse response = client.Post<ClientResponse>(createRequest);
response = client.Put<ClientResponse>(updateRequest);
Ниже представлен весь код метода
Post
класса SoapServiceClient
.public TResponse Post<TResponse>(object request)
{
return Send<TResponse>(request, OperationTypeHeader.Post);
}
private TResponse Send<TResponse>(object request, MessageHeader operationType)
{
using (var factory = new ChannelFactory<ISoapService>(_endpointConfigurationName))
{
MessageVersion messageVersion = factory.Endpoint.Binding.MessageVersion;
Message message = CreateMessage(request, operationType, messageVersion);
ISoapService channel = factory.CreateChannel();
Message result = channel.Process(message);
return result.GetBody<TResponse>();
}
}
private static Message CreateMessage(
object request, MessageHeader actionHeader, MessageVersion messageVersion)
{
Message message = Message.CreateMessage(
messageVersion, ServiceMetadata.Operations.Process, request);
var contentTypeHeader = new ContentTypeHeader(request.GetType());
message.Headers.Add(contentTypeHeader);
message.Headers.Add(actionHeader);
return message;
}
Обратите, пожалуйста, внимание на метод CreateMessage
и на то, как тип DTO и вызываемый метод добавляются через contentTypeHeader
and actionHeader
.SoapContentTypeHeader
и SoapOperationTypeHeader
практически идентичны. The SoapContentTypeHeader
используется для передачи типа DTO, а SoapOperationTypeHeader
— для передачи целевой операции. Меньше слов, больше кода:internal sealed class SoapContentTypeHeader : MessageHeader
{
private const string NameValue = "nelibur-content-type";
private const string NamespaceValue = "http://nelibur.org/" + NameValue;
private readonly string _contentType;
public SoapContentTypeHeader(Type contentType)
{
_contentType = contentType.Name;
}
public override string Name
{
get { return NameValue; }
}
public override string Namespace
{
get { return NamespaceValue; }
}
public static string ReadHeader(Message request)
{
int headerPosition = request.Headers.FindHeader(NameValue, NamespaceValue);
if (headerPosition == -1)
{
return null;
}
var content = request.Headers.GetHeader<string>(headerPosition);
return content;
}
protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteString(_contentType);
}
}
Ниже представлены методы
SoapServiceClient
:public static TResponse Get<TResponse>(object request)
public static Task<TResponse> GetAsync<TResponse>(object request)
public static void Post(object request)
public static Task PostAsync(object request)
public static TResponse Post<TResponse>(object request)
public static Task<TResponse> PostAsync<TResponse>(object request)
public static void Put(object request)
public static Task PutAsync(object request)
public static TResponse Put<TResponse>(object request)
public static Task<TResponse> PutAsync<TResponse>(object request)
public static void Delete(object request)
public static Task DeleteAsync(object request)
Как вы уже заметили, все CRUD операции имеют асинхронные версии.
SOAP сервис
SOAP сервис должен уметь:
- Создать конкретный Request из Message
- Вызвать целевой метод на Request
- При необходимости создать и вернуть Message из Response
Наша цель — создать что-то такое, что будет вызывать подходящий CRUD-метод для конкретного
Request
. В примере ниже показано, как можно добавлять и получать объект Client
(клиента).public sealed class ClientProcessor : IPut<CreateClientRequest>,
IGet<GetClientRequest>
{
private readonly List<Client> _clients = new List<Client>();
public object Get(GetClientRequest request)
{
Client client = _clients.Single(x => x.Id == request.Id);
return new ClientResponse {Id = client.Id, Name = client.Name};
}
public object Put(CreateClientRequest request)
{
var client = new Client
{
Id = Guid.NewGuid(),
Name = request.Name
};
_clients.Add(client);
return new ClientResponse {Id = client.Id};
}
}
Наибольший интерес представляют интерфейсы
IGet
и IPost
. Они представляют операции CRUD. Взглянем на диаграмму классов:Теперь необходимо связать
Request
с соответствующей операцией CRUD. Самый простой путь — связать Request
с обработчиком запросов (request Processor
). За эту функциональность отличает NeliburService
. Давайте взглянем на него.public abstract class NeliburService
{
internal static readonly RequestMetadataMap _requests = new RequestMetadataMap();
protected static readonly Configuration _configuration = new Configuration();
private static readonly RequestProcessorMap _requestProcessors = new RequestProcessorMap();
protected static void ProcessOneWay(RequestMetadata requestMetaData)
{
IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type);
processor.ProcessOneWay(requestMetaData);
}
protected static Message Process(RequestMetadata requestMetaData)
{
IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type);
return processor.Process(requestMetaData);
}
protected sealed class Configuration : IConfiguration
{
public void Bind<TRequest, TProcessor>(Func<TProcessor> creator)
where TRequest : class
where TProcessor : IRequestOperation
{
if (creator == null)
{
throw Error.ArgumentNull("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());
}
}
}
RequestMetadataMap
используется для хранения типа объекта Request
, который требуется для создания конкретного Request
из 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 NeliburService", typeName);
throw Error.InvalidOperation(errorMessage);
}
}
RequestProcessorMap
cсвязывает тип объекта Request
с обработчиком.internal sealed class RequestProcessorMap
{
private readonly Dictionary<Type, IRequestProcessor> _repository =
new Dictionary<Type, IRequestProcessor>();
public void Add<TRequest, TProcessor>(Func<TProcessor> creator)
where TRequest : class
where TProcessor : IRequestOperation
{
Type requestType = typeof(TRequest);
IRequestProcessor context = new RequestProcessor<TRequest, TProcessor>(creator);
_repository[requestType] = context;
}
public IRequestProcessor Get(Type requestType)
{
return _repository[requestType];
}
}
Теперь мы готовы для последнего шага: вызова целевого метода. Вот наш SOAP-сервис:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public sealed class SoapService : ISoapService
{
public Message Process(Message message)
{
return NeliburSoapService.Process(message);
}
public void ProcessOneWay(Message message)
{
NeliburSoapService.ProcessOneWay(message);
}
}
Прежде всего давайте посмотрим на диаграмму последовательности, описывающую процесс выполнения на стороне сервиса.
Давайте погрузимся в код шаг за шагом.
NeliburSoapService
просто выполняет другой код, взглянем на него.public sealed class NeliburSoapService : NeliburService
{
private NeliburSoapService()
{
}
public static IConfiguration Configure(Action<IConfiguration> action)
{
action(_configuration);
return _configuration;
}
public static Message Process(Message message)
{
RequestMetadata metadata = _requests.FromSoapMessage(message);
return Process(metadata);
}
public static void ProcessOneWay(Message message)
{
RequestMetadata metadata = _requests.FromSoapMessage(message);
ProcessOneWay(metadata);
}
}
NeliburSoapService
просто декорирует RequestMetadataMap
, то есть вызывает соответствующий метод для создания RequestMetadata
для SOAP Message
.Самое интересное происходит здесь:
RequestMetadata requestMetaData = _requests.FromSoapMessage(message)
context.Process(requestMetaData).
SoapRequestMetadata — это главный объект, который соединяет в себе тип операции CRUD, данные запроса (Request), его тип, а также может отвечать на запрос.
internal sealed class SoapRequestMetadata : RequestMetadata
{
private readonly MessageVersion _messageVersion;
private readonly object _request;
internal SoapRequestMetadata(Message message, Type targetType) : base(targetType)
{
_messageVersion = message.Version;
_request = CreateRequest(message, targetType);
OperationType = SoapOperationTypeHeader.ReadHeader(message);
}
public override string OperationType { get; protected set; }
public override Message CreateResponse(object response)
{
return Message.CreateMessage(_messageVersion, SoapServiceMetadata.Action.ProcessResponse, response);
}
public override TRequest GetRequest<TRequest>()
{
return (TRequest)_request;
}
private static object CreateRequest(Message message, Type targetType)
{
using (XmlDictionaryReader reader = message.GetReaderAtBodyContents())
{
var serializer = new DataContractSerializer(targetType);
return serializer.ReadObject(reader);
}
}
}
А в конце мы просто вызываем соответствующую CRUD-операцию через
RequestProcessor
. RequestProcessor
использует RequestMetadata
для определения операции и вызывает ее, когда возвращает результат классу SoapServiceClient
.internal sealed class RequestProcessor<TRequest, TProcessor> : IRequestProcessor
where TRequest : class
where TProcessor : IRequestOperation
{
private readonly Func<TProcessor> _creator;
public RequestProcessor(Func<TProcessor> creator)
{
_creator = creator;
}
public Message Process(RequestMetadata metadata)
{
switch (metadata.OperationType)
{
case OperationType.Get:
return Get(metadata);
case OperationType.Post:
return Post(metadata);
case OperationType.Put:
return Put(metadata);
case OperationType.Delete:
return Delete(metadata);
default:
string message = string.Format("Invalid operation type: {0}", metadata.OperationType);
throw Error.InvalidOperation(message);
}
}
public void ProcessOneWay(RequestMetadata metadata)
{
switch (metadata.OperationType)
{
case OperationType.Get:
GetOneWay(metadata);
break;
case OperationType.Post:
PostOneWay(metadata);
break;
case OperationType.Put:
PutOneWay(metadata);
break;
case OperationType.Delete:
DeleteOneWay(metadata);
break;
default:
string message = string.Format("Invalid operation type: {0}", metadata.OperationType);
throw Error.InvalidOperation(message);
}
}
private Message Delete(RequestMetadata metadata)
{
var service = (IDelete<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
object result = service.Delete(request);
return metadata.CreateResponse(result);
}
private void DeleteOneWay(RequestMetadata metadata)
{
var service = (IDeleteOneWay<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
service.DeleteOneWay(request);
}
private Message Get(RequestMetadata metadata)
{
var service = (IGet<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
object result = service.Get(request);
return metadata.CreateResponse(result);
}
private void GetOneWay(RequestMetadata metadata)
{
var service = (IGetOneWay<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
service.GetOneWay(request);
}
private Message Post(RequestMetadata metadata)
{
var service = (IPost<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
object result = service.Post(request);
return metadata.CreateResponse(result);
}
private void PostOneWay(RequestMetadata metadata)
{
var service = (IPostOneWay<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
service.PostOneWay(request);
}
private Message Put(RequestMetadata metadata)
{
var service = (IPut<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
object result = service.Put(request);
return metadata.CreateResponse(result);
}
private void PutOneWay(RequestMetadata metadata)
{
var service = (IPutOneWay<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
service.PutOneWay(request);
}
}
Демонстрационный пример
Прежде всего, объявим data contracts:
CreateClientRequest
— запрос на создание нового клиентаUpdateClientRequest
— запрос на обновление email клиентаGetClientRequest
— запрос на получение клиента по idClientResponse
— информация о клиентеRemoveClientRequest
— запрос на удаление клиента
Server's side
Конфигурационный файл самый обычный:
<configuration>
<!--WCF-->
<system.serviceModel>
<services>
<service name="Nelibur.ServiceModel.Services.Default.SoapServicePerCall">
<endpoint address="http://localhost:5060/service" binding="basicHttpBinding"
bindingConfiguration="ServiceBinding"
contract="Nelibur.ServiceModel.Contracts.ISoapService" />
</service>
</services>
<bindings>
<basicHttpBinding>
<binding name="ServiceBinding">
<security mode="None">
<transport clientCredentialType="None" />
</security>
</binding>
</basicHttpBinding>
</bindings>
</system.serviceModel>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>
WCF-сервис чрезвычайно прост:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public sealed class SoapServicePerCall : ISoapService
{
/// <summary>
/// Process message with response.
/// </summary>
/// <param name="message">Request message.</param>
/// <returns>Response message.</returns>
public Message Process(Message message)
{
return NeliburSoapService.Process(message);
}
/// <summary>
/// Process message without response.
/// </summary>
/// <param name="message">Request message.</param>
public void ProcessOneWay(Message message)
{
NeliburSoapService.ProcessOneWay(message);
}
}
Привязка всех запросов к обработчикам. Для простоты я создал только один обработчик запросов. Вы можите сделать столько запросов, сколько захотите. Советую почитать статью Мартина Фаулера о CQRS. Это поможет вам сделать правильный выбор. Код связи запросов и обработчиков:
private static void BindRequestToProcessors()
{
NeliburSoapService.Configure(x =>
{
x.Bind<CreateClientRequest, ClientProcessor>();
x.Bind<UpdateClientRequest, ClientProcessor>();
x.Bind<DeleteClientRequest, ClientProcessor>();
x.Bind<GetClientRequest, ClientProcessor>();
});
}
И, наконец,
ClientProcessor
:public sealed class ClientProcessor : IPost<CreateClientRequest>,
IGet<GetClientRequest>,
IDeleteOneWay<DeleteClientRequest>,
IPut<UpdateClientRequest>
{
private static List<Client> _clients = new List<Client>();
public void DeleteOneWay(DeleteClientRequest request)
{
Console.WriteLine("Delete Request: {0}\n", request);
_clients = _clients.Where(x => x.Id != request.Id).ToList();
}
public object Get(GetClientRequest request)
{
Console.WriteLine("Get Request: {0}", request);
Client client = _clients.Single(x => x.Id == request.Id);
return new ClientResponse { Id = client.Id, Email = client.Email };
}
public object Post(CreateClientRequest request)
{
Console.WriteLine("Post Request: {0}", request);
var client = new Client
{
Id = Guid.NewGuid(),
Email = request.Email
};
_clients.Add(client);
return new ClientResponse { Id = client.Id, Email = client.Email };
}
public object Put(UpdateClientRequest request)
{
Console.WriteLine("Put Request: {0}", request);
Client client = _clients.Single(x => x.Id == request.Id);
client.Email = request.Email;
return new ClientResponse { Id = client.Id, Email = client.Email };
}
}
Client's side
Код клиента прост:
private static void Main()
{
var client = new SoapServiceClient("NeliburSoapService");
var createRequest = new CreateClientRequest
{
Email = "email@email.com"
};
Console.WriteLine("POST Request: {0}", createRequest);
ClientResponse response = client.Post<ClientResponse>(createRequest);
Console.WriteLine("POST Response: {0}\n", response);
var updateRequest = new UpdateClientRequest
{
Email = "new@email.com",
Id = response.Id
};
Console.WriteLine("PUT Request: {0}", updateRequest);
response = client.Put<ClientResponse>(updateRequest);
Console.WriteLine("PUT Response: {0}\n", response);
var getClientRequest = new GetClientRequest
{
Id = response.Id
};
Console.WriteLine("GET Request: {0}", getClientRequest);
response = client.Get<ClientResponse>(getClientRequest);
Console.WriteLine("GET Response: {0}\n", response);
var deleteRequest = new DeleteClientRequest
{
Id = response.Id
};
Console.WriteLine("DELETE Request: {0}", deleteRequest);
client.Delete(deleteRequest);
Console.ReadKey();
}
Результаты выполнения:
клиент:
сервис:
Вот и все
Я надеюсь, что вам понравилось. Здесь вы можете узнать, как строить RESTful веб-сервисы на WCF и Nelibur. Спасибо, что прочли статью (перевод). Исходники можно скачать со страницы оригинала или с GitHub.