65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (50投票s)

2013年8月12日

MIT

6分钟阅读

viewsIcon

194246

downloadIcon

3089

如何使用 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);
} 

其中 RequestResponse 可以是任何 DTO,也就是说,通过一个方法,我们可以替换任何 RPC 服务协定,但 WCF 使用 RPC 风格。

基于消息的风格

您知道,对于基于消息的服务,我们可以使用 RequestResponse 对象来传输任何 DTO。但是 WCF 不支持这种设计。所有内部 WCF 通信都基于 Message 类,也就是说,WCF 将任何 DTO 转换为 Message 并将 Message 从客户端发送到服务器。因此,我们应该使用 Message 类作为 RequestResponse 对象。

以下服务协定描述了带或不带 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 创建 MessageSoapServiceClient 是一个包装器,它将任何 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);  

这是完整的 SoapServiceClientPost 方法,请注意 CreateMessage 方法以及通过 contentTypeHeaderactionHeader 如何添加具体的 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;
} 

SoapContentTypeHeaderSoapOperationTypeHeader 几乎相同。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};
    }
}

最有趣的部分:IGetIPost 接口。这些接口代表 CRUD 操作,这是类图。注意 I<Operation>I<Operation>OneWay 之间的区别。例如,IPostIPostOneWayIPostOneWay 返回 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);
    }
} 

RequestProcessorMapRequest 的类型与请求处理器绑定。

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)
© . All rights reserved.