使用 WCF 构建 SOAP 基于消息的 Web 服务






4.96/5 (50投票s)
如何使用 WCF 构建 SOAP 基于消息的 Web 服务。
引言
我非常喜欢 WCF 作为技术框架,它简化了通信层的创建,但我不喜欢 WCF 的设计风格。我认为为每个数据传输对象创建一个新方法很糟糕,所以我试图解决这个问题。
WCF 本质上是基于方法的 Web 服务,并存在一些限制:
- 不支持方法重载
- 没有通用 API
- 服务协定取决于业务需求
- 版本控制应在
DataContract
和方法级别进行,操作名称应通用 - 其他非 .NET 客户端必须创建与服务数量一样多的客户端
我认为 RPC(远程过程调用)方法不是正确的方式,服务应该是可重用的,并且需求的影响应该是最小的。对我来说,远程 API 必须满足以下标准:
- 稳定且通用的接口
- 根据 DTO 模式 传输数据
基于消息的服务通过添加消息抽象来解决主要的 WCF 限制。
剧透
阅读本文后,您将了解如何构建可重用的基于消息的 SOAP 服务、SOAP 客户端,以及**停止创建新的 WCF 服务**。
Web 服务设计
让我们更深入地探讨远程过程调用和基于消息的方法。
RPC 设计
RPC 风格的主要思想是共享方法,因此客户端像使用本地对象一样使用远程服务。WCF 的 ServiceContract
指定了客户端可用的操作。
例如:
[ServiceContract]
public interface IRpcService
{
[OperationContract]
void RegisterClient(Client client);
[OperationContract]
Client GetClientByName(string clientName);
[OperationContract]
List<Client> GetAllClients();
}
服务协定非常简单,包含三个操作。在服务协定中的任何更改(例如,添加或删除操作、更新操作签名)后,我们都必须更改服务/客户端。实际应用程序可能有超过 10 个操作,因此维护服务和客户端非常困难。
基于消息的设计
基于消息设计的核心思想是 Martin Fowler 的模式:数据传输对象和网关。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,也就是说,通过一个方法,我们可以替换任何 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 操作(创建、读取、更新、删除),因此我们可以实现任何操作。首先,让我们创建能够发送和接收任何 DTO 的 SoapServiceClient
。
SOAP 服务客户端
SoapServiceClient
将说明如何从 DTO 创建 Message
。SoapServiceClient
是一个包装器,它将任何 DTO 转换为 Message
并将其发送到服务。发送的 Message
应包含以下数据:
- DTO
- DTO 的类型,在服务器端进行 DTO 反序列化时需要
- 目标方法,将在服务器端调用
我们的目标是创建一个可重用的 soap 服务客户端,它能够发送/接收任何 Request/Response
并执行任何目标操作。如前所述,CRUD 操作是最佳选择,因此客户端可以如下所示:
var client = new SoapServiceClient("NeliburSoapService");
ClientResponse response = client.Post<ClientResponse>(createRequest);
response = client.Put<ClientResponse>(updateRequest);
这是完整的 SoapServiceClient
的 Post
方法,请注意 CreateMessage
方法以及通过 contentTypeHeader
和 actionHeader
如何添加具体的 DTO 类型和目标方法。
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;
}
SoapContentTypeHeader
和 SoapOperationTypeHeader
几乎相同。SoapContentTypeHeader
用于传输 DTO 类型,SoapOperationTypeHeader
用于传输目标操作。对于 MessageHeader
,没有太多可说的,这是 SoapContentTypeHeader
的完整代码。
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 操作都有 async
版本。
SOAP 服务
SoapService 应能够执行以下操作:
- 从
Message
创建具体的Request
- 通过
Request
调用目标消息 - 从
Response
创建Message
并按需返回
我们的目标是创建一个可以根据具体 Request
调用相应 CRUD 方法的工具,此示例说明了如何添加和获取客户端。
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 操作,这是类图。注意 I<Operation>
和 I<Operation>OneWay
之间的区别。例如,IPost
和 IPostOneWay
,IPostOneWay
返回 void
。
现在,我们需要做的就是将 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());
}
}
}
具体的 NeliburSoapService
只包含处理和配置方法,我们稍后会看到。
RequestMetadataMap
用于存储 Request
的类型,这在从 Message
创建具体 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
的类型与请求处理器绑定。
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];
}
}
现在我们准备好进行最后一步——调用目标方法。这是我们的 SoapService
,与普通的 WCF 服务一样。
[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
,即调用相应的方法来为 soap 消息创建 RequestMetadata
。
最有趣的事情发生在这里。
RequestMetadata metadata = _requests.FromSoapMessage(message)
.Process(metadata)
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
使用 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="https://: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>
Nelibur 已包含默认的 SoapServicePerCall
服务,这是其实现。
[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);
}
}
将所有请求与请求处理器绑定。为简单起见,我只创建了一个请求处理器。您可以创建任意数量的请求处理器。查看 Martin Fowler 关于 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 };
}
}
客户端
客户端代码尽可能简单。
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();
}
以及执行结果。
这是服务客户端。
以及 SOAP 服务。
各位,就这些了
希望您喜欢它,请花时间发表评论。在这里,您可以了解如何使用 WCF 构建基于 RESTful 消息的 Web 服务。感谢阅读本文。
历史
- 2013 年 8 月 12 日
- 初始版本
- 2014 年 5 月 17 日
- 更改了客户端方法的签名。例如:
client.Get<GetClientRequest, ClientResponse>(request)
=>client.Get<ClientResponse>(request)