WCFを使用したメッセージベースのWebサービスSOAPの構築

私は、コミュニケーション層の作成を簡素化するフレームワークとしてのWCFが本当に好きです。 しかし、WCFのデザインスタイルは私には適していません。 DTOごとに新しいメソッドを作成するのは良い解決策ではないと思うので、この問題を解決しようとしました。



WCFにはいくつかの制限があります。



RPC(リモートプロシージャコール)スタイルのアプローチは最適ではないと思います。 サービスは再利用可能でなければならず、ビジネス要件がサービスに与える影響は最小限でなければなりません。 リモートAPIは次の要件を満たす必要があると思います。



メッセージベースのWebサービスは、メッセージの抽象化を追加することにより、WCFの制限のほとんどを克服します。

この記事を読んだ後、再利用可能なSOAPメッセージベースのWebサービスを構築する方法を学習します(常に新しいサービスの作成を停止します)。



Webサービスの設計



RPCスタイルのアプローチと、メッセージベースのアプローチを見てみましょう。



RPCデザイン



RPCスタイルの主なアイデアは、顧客にリモートサービスをローカルオブジェクトとして使用する機会を提供することです。 WCFでは、ServiceContractはクライアント側で利用可能な操作を定義します。 例:

[ServiceContract] public interface IRpcService { [OperationContract] void RegisterClient(Client client); [OperationContract] Client GetClientByName(string clientName); [OperationContract] List<Client> GetAllClients(); }
      
      





サービス契約は非常にシンプルで、3つの操作が含まれています。 サービス契約の変更(たとえば、操作の追加または削除、操作の署名の変更)後にクライアントを変更する必要があります。 実際のアプリケーションには10を超える操作があるため、サービスと顧客の維持には非常に時間がかかります。



メッセージベースのデザイン



メッセージベースのアプローチは、 データ転送オブジェクトゲートウェイのパターンに基づいています。 DTOには通信に必要なすべてのデータが含まれ、ゲートウェイは通信プロセスからアプリケーションを分離します。 したがって、メッセージベースのサービスは要求メッセージを受信し、応答メッセージを返します。 Amazon APIの例を見てみましょう。



リクエストの例:

 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にできます。つまり、1つの方法でRPCサービスコントラクトを置き換えることができますが、WCFはRPCスタイルを使用します。



メッセージベースのスタイル



ご存知のように、メッセージベースのWebサービスでは、 Request



オブジェクトとResponse



オブジェクトを使用して、DTOを送信できます。 ただし、WCFはこの設計をサポートしていません。 WCFのすべての通信内部は、Messageクラスの使用に基づいています。 つまり、WCFはDTOをMessage



インスタンスに変換し、クライアントからサーバーにMessage



を送信します。 したがって、 Request



オブジェクトとResponse



オブジェクトにはMessage



クラスを使用する必要があります。

次のサービスコントラクトは、 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操作が最良の選択であるため、任意の操作を実装できます。 まず、DTOを送受信できるSoapServiceClient



を作成しましょう。



石鹸サービスクライアント



SoapServiceClient



は、DTOからMessage



を作成する方法を示します。 SoapServiceClient



は、DTOをMessage



変換してサービスに送信するラッパーです。 送信するメッセージには次のデータが含まれます。

私たちの目標は、SOAP Webサービス用の再利用可能なクライアントを作成することです。これにより、要求/応答を送受信し、オブジェクトに対して操作を実行できます。 前述のように、CRUDはこれに最も適しているため、クライアントは次のようになります。

 var client = new SoapServiceClient("NeliburSoapService"); ClientResponse response = client.Post<ClientResponse>(createRequest); response = client.Put<ClientResponse>(updateRequest);
      
      





以下は、 SoapServiceClient



クラスのPost



メソッドのすべてのコードです。

 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



およびactionHeader



介して追加される方法に注意してください。

SoapContentTypeHeader



SoapOperationTypeHeader



ほぼ同じです。 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



に対して適切なCRUDメソッドを呼び出すものを作成することです。 以下の例は、 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



をリクエストプロセッサに関連付けることです。 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



Message



から特定のRequest



を作成するために必要なRequest



オブジェクトのタイプを格納するために使用されます。

 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



は、 Request



オブジェクトのタイプをRequestProcessorMap



バインドします。

 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



を作成します。

ここで最も興味深いことが起こっています:



SoapRequestMetadataは、CRUD操作のタイプ、リクエストデータ、そのタイプを組み合わせたメインオブジェクトであり、リクエストに応答することもできます。

 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); } } }
      
      





最後に、 RequestProcessor



介して対応する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); } }
      
      





デモ



まず、データコントラクトを宣言します。





サーバー側



構成ファイルが最も一般的です。

 <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); } }
      
      





すべてのリクエストをハンドラーにバインドします。 簡単にするために、要求ハンドラーを1つだけ作成しました。 必要な数の要求を作成できます。 CQRSに関するMartin Fowlerの記事を読むことをお勧めします。 これは、正しい選択をするのに役立ちます。 リクエストとハンドラーをリンクするためのコード:

 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 }; } }
      
      







クライアント側



クライアントコードは簡単です。

 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(); }
      
      







実行結果:

顧客:

クライアント



サービス:

奉仕



それだけです



楽しんでいただけましたでしょうか。 ここでは、WCFおよびNeliburでRESTful Webサービスを構築する方法を学ぶことができます。 記事を読んでいただきありがとうございます(翻訳)。 ソースは、 元のページまたはGitHubからダウンロードできます。



All Articles