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

让 WCF 发挥作用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2016年1月14日

CPOL

7分钟阅读

viewsIcon

15401

downloadIcon

187

用于大规模 WCF 服务层开发的框架

引言

本文总结了我们在使用 WCF 创建 Web 服务层时遇到的问题以及我们使用的解决方案。所有这些都整齐地归结为一组包含代码和工具的程序集,这些程序集可以作为您的服务层的基础设施,或者为您新的开发提供灵感。

发布本文的主要动机之一是获取反馈。请不要吝啬您的意见。

明智之举:近期,ASP.NET Web API 中包含的 RESTful 服务似乎成为 Microsoft 通信堆栈中的首选。虽然 WCF 肯定更适合某些场景,但这一点应该被考虑在内。做出明智的决定。

背景

在我们开始开发 WCF 服务层时,我们希望解决以下问题:

  • 避免样板代码
  • 为大型项目提供统一的组织结构
  • 提供同步和异步功能
  • 尽可能避免保留状态
  • 提供错误处理基础设施
  • 支持可测试性
  • 支持身份验证和授权
  • 维护传输和托管无关的服务

我们将在下面逐一解决这些问题。

Using the Code

您可能不想直接“使用代码”,但这是您使用它时会做的事情

步骤 1:准备您的解决方案

在您的解决方案中,您将拥有以下项目:

  • Operations - 您的服务代码。开发工作在此处进行(引用 Ziv.ServiceModel - 核心运行时程序集)。
  • Contracts - 包含服务代码的程序集。此处的所有代码均使用内置的 Visual Studio 代码生成平台 - T4 模板(引用 Ziv.ServiceModelZiv.ServiceModel.CodeGenerationOperations 项目)自动生成。
  • Services - 包含服务代码的程序集。此处代码也为自动生成(引用 Contracts 项目及其所有依赖项)。
  • Host - 已部署的托管项目 - 主要负责引导整个系统,通常是您的 IIS 项目(引用核心、Operations 和生成的代码。不需要 Ziv.ServiceModel.CodeGeneration 代码生成)。

有关完整示例,请下载本文附加的解决方案,或在 GitHub 上查看。

步骤 2:编写您的代码

在您的 Operations 项目中,对于您要公开的每个服务方法,请执行以下操作:

  • 创建一个派生自 OperationBase<TResult> 的类,并用 OperationAttribute 装饰它。
  • 创建一个构造函数,它接受操作所需的参数(如果有)、您需要传递给基类构造函数的 IOperationsManager 实例,以及执行任务所需的依赖项(顺序很重要,稍后解释)。
    确保将所需的参数和依赖项保留在 private 类字段中。
  • 实现 abstractRun() 方法。

操作示例

[Operation("Calculator")]
public class AddOperation : OperationBase<double>
{
    private ICalculatorComponent _calculator;
    private AddOperationParameters _parameters;
    
    public AddOperation(
                AddOperationParameters parameters,
                IOperationsManager operationsManager,
                ICalculatorComponent calculator)
         : base(operationsManager)
    {
        _calculator = calculator;
        _parameters = parameters;
    }

    protected override double Run()
    {
        return _calculator.Add(_parameters.Addend1, _parameters.Addend2);
    }     
}

步骤 3:生成服务和契约

ContractsServices 项目中,执行以下操作:

  • 生成项目(此时项目应为空),这将导致引用的程序集被复制到项目目标文件夹。
  • 创建一个文本模板(*.tt)文件。
  • 在此文件中,引用 Operations 项目程序集,并包含 CodeGeneration 库中的相应模板(请参阅下面的示例)。
  • 右键单击 *.tt 文件并运行自定义工具。

T4 模板将运行,并在新的 *.cs 文件中生成您的契约接口和服务类。

Contracts.tt 文件的示例...

<#@ assembly name="$(SolutionDir)\Sample.Operations\bin\Debug\Sample.Operations.dll" #>
<#@ include file="$(SolutionDir)\Ziv.ServiceModel.CodeGeneration\Templates\Services.ttinclude" #>

...以及它将生成的代码

[ServiceContract]
public interface ICalculatorService
{
     #region Operation Add

     [OperationContract]
     double Add(AddOperationParameters parameters);

     #endregion
}

请参阅下方和附加解决方案中的更多示例。

步骤 4:托管您的服务

您可以使用 WCF 在任何部署方案中托管生成的类。您所需要做的就是提供 Ziv.ServiceModel.Activation.DependencyInjectionServiceHostFactory 作为您的服务主机工厂,并通过调用 ServiceLocator.SetServiceLocator() 来注册您的依赖项注入框架。

您还应该向依赖项注入容器注册一个 IOperationsManager 实例(必须是单例)。

这是使用 Unity 作为 DI 框架的控制台托管项目的非常简单的示例

static void Main(string[] args)
{
    // Setup DI configuration:
    IUnityContainer container = new UnityContainer();    
    container.RegisterType<IOperationsManager, SingleProcessDeploymentOperationsManager>(
        new ContainerControlledLifetimeManager());
    container.RegisterType<ICalculatorComponent, CalculatorComponentImpl>();
    ServiceLocator.SetServiceLocator(new UnityServiceLocator(container));

    // Setup hosting:
    var factory = new DependencyInjectionServiceHostFactory();
    var baseAddress = new Uri("https://:54321/services");
    var serviceHost = factory.CreateServiceHost(
                                typeof(CalculatorService).AssemblyQualifiedName, 
                                new Uri[] { baseAddress });
    serviceHost.Open();

    Console.ReadKey();
}

您可以在附加的示例解决方案中找到更适合生产环境的基于 Web 的托管示例。您还可以在托管项目中的 web.tt 上运行自定义工具,这样您就拥有了一个运行的托管解决方案,所有新功能都已准备好提供。

工作原理

操作类以 WCF 不可知的方式开发,所有连接均使用标准的 Visual Studio T4 模板进行生成。IOperationsManager 是提供此整个操作的管理服务的粘合剂。

避免样板代码

当您在操作类中编写业务逻辑时,您可以完全专注于它。所有周边工作都为您完成了。

  • SOAP 契约和相应的 WCF 服务类通过 T4 模板根据操作签名自动生成。
  • 异步调用、操作状态管理和错误处理 - 都由 OperationBase<T> 和注入的 IOperationManager 完成。

为大型项目提供统一的组织结构

多个操作在分层结构中的排列几乎是自动的。您的操作的命名空间层次结构将反映在自动生成的代码(契约和服务类)的命名空间中以及访问服务所用的 URL 模式中。要合并多个操作到一个服务中,您可以在其 OperationAttribute 中为每个操作提供相同的“短名称”。

[Operation("Calculator")]
public class AddOperation : OperationBase<double>
{
    // Class body omitted for briefly...
}

[Operation("Calculator")]
public class SubtractOperation : OperationBase<double>
{
    // Class body omitted for briefly...
}

这将生成如下契约

[ServiceContract]
public interface ICalculatorService     
{         
    #region Operation Add

    [OperationContract]
    double Add(AddOperationParameters parameters);

    #endregion

    #region Operation Subtract

    [OperationContract]
    double Subtract(SubtractOperationParameters parameters);

    #endregion
}

提供同步和异步功能

使用 OperationAttribute.Generate 来请求在您公开的契约(和 服务)上进行同步和/或异步操作。

[Operation("Calculator", Generate = OperationGeneration.Both)]
public class AddOperation : OperationBase<double>
{
    // Class body omitted for briefly...
}

生成契约

[ServiceContract]
public interface ICalculatorService     
{         
    #region Operation Add

    [OperationContract]
    double Add(AddOperationParameters parameters);

    [OperationContract]
    OperationStartInformation AddAsync(AddOperationParameters parameters);

    [OperationContract]
    OperationStatus<double> AddGetStatus(Guid operationId);

    [OperationContract]
    void AddCancel(Guid operationId);

    #endregion
}

在长时间运行的操作过程中,从您的操作代码调用 ReportProgress() 来报告进度;调用 IsCancelationPending() 来检查用户是否请求了取消,并通过调用 ReportCancelationCompleted() 来报告取消已完成。这些方法定义在 OperationBase<T> 上,并将所有进展报告给 IOperationsManager

对于长时间运行的操作,您还可以使用 OperationAttribute 为客户端提供一些数据,以帮助其决定何时检查结果以及何时轮询进度报告。

[Operation("Calculator",
    Generate = OperationGeneration.Async,
    IsReportingProgress = true,
    IsSupportingCancel = true,    
    ExpectedCompletionTimeMilliseconds = 60000,    
    SuggestedPollingIntervalMilliseconds = 2000
)]
public class AddOperation : OperationBase<double>
{
    // Class body omitted for briefly...
?}

当客户端调用 AddAsync 方法时,它将在返回的 OperationStartInformation 中收到这些信息。

尽可能避免保留状态

操作预计不会保留任何状态。所有状态,包括返回值、错误、进度状态和取消状态,都由传递给 OperationBase<T>IOperationsManager 管理。这样,长时间运行的操作只需执行其应有的操作,而不必处理客户端轮询当前状态的问题,这对于 HTTP 等请求-响应协议非常有用。

依赖于其他代码组件的操作应使用依赖项注入来告知系统其所需,系统将通过 ServiceLocator.SetServiceLocator() 设置的依赖项解析器来确保提供这些依赖项。

为了使 DI 机制与其他操作的输入良好协同工作,操作构造函数应按以下顺序接受参数:

  1. 请求参数(由使用服务的方提供)
  2. 一个 IOperationsManager 实例,用于传递给 OperationBase<T>
  3. 依赖项注入的参数

为了更好地说明我们的 AddOperation 示例,这里(再次)是操作代码...

[Operation("Calculator")]
public class AddOperation : OperationBase<double>
{
    private ICalculatorComponent _calculator;
    private AddOperationParameters _parameters;
    
    public AddOperation(
                AddOperationParameters parameters,
                IOperationsManager operationsManager,
                ICalculatorComponent calculator)
         : base(operationsManager)
    {
        _calculator = calculator;
        _parameters = parameters;
    }

    protected override double Run()
    {
        return _calculator.Add(_parameters.Addend1, _parameters.Addend2);
    }     
}

...以及基于它自动生成的服务类

public sealed class CalculatorService : ServiceBase, ICalculatorService
{
    private readonly IOperationsManager _operationsManager;
    private readonly ICalculatorComponent _calculator;

    public CalculatorService(IOperationsManager operationsManager, ICalculatorComponent calculator)
        : base(operationsManager)
    {
         _operationsManager = operationsManager;
         _calculator = dataApplicationProvider;
    }

    #region Operation Add

    public double Add(AddOperationParameters parameters)
    {
        return (double)DoOperation(GetOperationAdd(parameters)).Result;
    }

    private AddOperation GetOperationAdd(AddOperationParameters parameters)
    {
        return new AddOperation(parameters, _operationsManager, _calculator);
    }

    #endregion
}

注意 AddOperation 构造函数的参数如何映射到生成服务类的各种元素。

提供错误处理基础设施

错误处理基于标准的 WCF/SOAP 错误处理。要用 FaultContractAttibute 装饰契约接口,请用 OperationFaultAttribute 装饰操作类。

[Operation("Calculator")]
[OperationFaultContract(typeof(CalculatorFault))]
public class AddOperation : OperationBase<double>
{
    // Class body omitted for briefly...
}

基于此操作,自动生成的契约可能如下所示:

[ServiceContract]
public interface ICalculatorService
{
     #region Operation Add

     [OperationContract]
     [FaultContract(typeof(CalculatorFault))]
     double Add(AddOperationParameters parameters);

     #endregion
}

支持可测试性

由于操作是无状态创建的,因此非常容易提供 IOperationsManager 和其他依赖项的模拟替代品,并将它们作为自动化测试的一部分运行。所有状态都可以从 IOperationsManager 模拟中观察到。

支持身份验证和授权

身份验证和授权依赖于 .NET 和 WCF 基础结构。即,用 PrincipalPermissionAttribute 装饰 Operation.Run()

维护传输和托管无关的服务

生成的服务是纯 WCF,不依赖于任何特定协议。身份验证和授权模式可用性的实现在 WCF 绑定之间有所不同,因此对于某些部署场景可能需要对管道进行一些调整。

结论

WCF 在抽象处理复杂的网络协议方面做得非常出色。然而,对于大型项目和复杂产品的开发管理而言,它仍然可能被认为是一个低级框架。我们的解决方案解决了更高级别的抽象,从而提高了服务层开发的效率。

请注意,我们库的当前阶段专注于分布式系统的服务器端。客户端得到了少量关注,以简化一些 WCF 的奇怪行为,例如故障状态对象的处理和处理方式,但这仍在进行中。

我们希望此库已实现其简化服务开发的同时保持其强大性的总体目标。如果您有任何想法、批评或评论,请告知我们。

历史

  • 版本 1.0 2016 年 1 月
© . All rights reserved.