WCFにはいくつかの制限があります。
- メソッドのオーバーロードはサポートしていません。
- ユニバーサルAPIはありません。
- サービス契約はビジネス要件に依存します。
- バージョニングはDataContractおよびメソッドのレベルで実行する必要があり、操作の名前はユニバーサルである必要があります。
- 他の非.NETクライアントは、サービスと同じ数のクライアントを作成する必要があります。
RPC(リモートプロシージャコール)スタイルのアプローチは最適ではないと思います。 サービスは再利用可能でなければならず、ビジネス要件がサービスに与える影響は最小限でなければなりません。 リモートAPIは次の要件を満たす必要があると思います。
- 安定した汎用性のあるインターフェースを備えています。
- DTOパターンに従ってデータを転送します。
メッセージベースの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
変換してサービスに送信するラッパーです。 送信するメッセージには次のデータが含まれます。
- DTO
- サーバー側の逆シリアル化に必要なDTOタイプ
- サーバー側で呼び出されるメソッド。
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
を作成します。
ここで最も興味深いことが起こっています:
RequestMetadata requestMetaData = _requests.FromSoapMessage(message)
context.Process(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); } }
デモ
まず、データコントラクトを宣言します。
-
CreateClientRequest
新しいクライアントを作成するリクエスト -
UpdateClientRequest
電子メールクライアントの更新要求 -
GetClientRequest
-IDによるクライアントの要求 -
ClientResponse
クライアント情報 -
RemoveClientRequest
クライアント削除リクエスト
サーバー側
構成ファイルが最も一般的です。
<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からダウンロードできます。