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 }
Save
method. public void Save(PresentRequest request)
Save
method returns void
, since we know that the service will be heavily loaded, so the generation of a unique Id
falls on the client’s shoulders.Save
method with the WebInvoke
attribute and specify the appropriate HTTP method. Here is a small cheat sheet for HTTP methods: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); }
PresentRequest
Save
methods as we will have different PresentRequest
objects. But what about OOP ?Create
, Update
and Delete
operations have similar pros and cons. The Get operation is different and appears, in my opinion, the most difficult to follow method.PresentRequest
by status and country, we need to create something like [WebGet(UriTemplate = "requests?country={country}&status={status}")] [OperationContract] List<PresentRequest> Get(string country, string status);
Get
method. Imagine that we use this method inside our application, without WCF. public interface IPresentRequestService { List<PresentRequest> Get(string country, string status); }
Get
operation is difficult to maintain by default. public interface IPresentRequestService { List<PresentRequest> Get(PresentRequestQuery query); }
PresentRequestQuery
class: public class PresentRequestQuery { public string Country { get; set; } public string Status { get; set; } }
Get
method has a fragile signature, so extending the functionality without breaking changes is really difficult. The parameters of a Get operation are sent as a query string with simple fields that are also represented in the signature of the Get
method. There is no connectivity between the parameters, since WCF does not create a request object based on parameters.PresentReuqest
s by country and status. public List<PresentRequest> Get(string country, string status) { throw new NotImplementedException(); }
country
and status
means, we can only guess. In my opinion, WCF should be able to create a request date based on the request object (serialize), and also create a request object based on the request string (deserialization). Thus, to send the following request object: public class PresentRequestQuery { public string Country { get; set; } public string Status { get; set; } }
country=sheldonopolis&status=pending
, and after receiving the query string must be deserialized into the PresentRequestQuery
instance and the Get
method should look like this: public List<PresentRequest> Get(PresentRequestQuery query) { throw new NotImplementedException(); }
[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) {...} ... }
Message Get (Message request);
Get
:Get
methods.KnownTypeAttribute
, but, in my opinion, WCF must support parametric polymorphism .Get
method. I think an approach based on serialization messages can help us.PresentRequestQuery
class, but now let's serialize it. public class PresentRequestQuery { public string Country { get; set; } public string Status { get; set; } }
Get
sends the parameters as a query string, so our serialization method should create a valid query string. The ideal query string resulting from serialization should look like this: country=sheldonopolis&status=pending
and we want to create something similar. The ideal result of serialization has one drawback: the lack of a connection between the parameters, so we cannot deserialize the URL into the request object. Our serialization mechanism should also solve this problem.key1=value1&key2=value2&key3=value3
.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
instance. The implementation is very simple: 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
calls only Uri.EscapeDataString
and JsonDataContractSerializer
is an instance of the 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()); } }
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); }
Get, Post, Put, Delete
and ServiceContract
can be like this: [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
has the flexibility, stability and ease of maintenance. We can transfer any data, because the service depends only on the Message
class, which is fundamental for WCF ( MSDN ). Another advantage is CRUD. Using IJsonService and URL serialization, we can create reusable RESTful services with parametric polymorphism . 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 }; } }
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
has only configuration and processing methods. 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
used to store the types of requests that are needed to create specific requests from Message
instances. 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); } }
RestRequestMetadata
, a class that helps create a specific request from a URL. Before looking at the implementation of RestRequestMetadata
, I want to give some explanations. RestRequestMetadata
uses WebOperationContext
to get the query string and create a specific query. It can also create a response message based on the request. 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; } }
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); } }
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
<?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
and IJsonService
have already been mentioned above.PresentRequestProcessor
will handle the PresentRequest
and the 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
shows Get, Post, Put and Delete requests for gifts: 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(); }
Source: https://habr.com/ru/post/218149/
All Articles