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

通用 WCF 服务主机和客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (20投票s)

May 2, 2010

CPOL

5分钟阅读

viewsIcon

268408

downloadIcon

4729

通用的 WCF Windows 服务宿主和客户端,配置最少,支持异步。

引言

厌倦了为使用标准绑定和传输的标准 Windows 服务宿主的通用 Windows Communication Foundation (WCF) 服务编写宿主和客户端(代理)代码,并且这些代码是通过容易出错的配置文件而不是直接在代码中配置的吗?我曾是。现在不是了。本文提出了一种解决此问题的方法:一个通用的 WCF Windows 服务宿主和客户端代理类库,它可以宿主并允许您使用您编写的任何 WCF 服务合同。您在一个类库中编写服务合同和数据合同,在另一个类库中实现操作合同。然后,修改通用服务及其客户端上一个简化的配置节,然后重新启动服务。简单快捷。

GenericWcfServiceHostAndClient.png

背景

Visual Studio 项目模板和配置工具可以使编写 WCF 服务变得更容易,但我发现自己越来越多地避免使用这些工具,而是手工编写一切,以便在没有那些繁重、有时会出问题的配置文件的情况下获得我想要的确切结果。每次我这样做时,我都会意识到这些步骤是相同的、乏味的。难道我们写代码就是为了避免重复的枯燥工作吗?

因此,我决定为宿主我的服务创建一个可重用的框架。以下是我的要求:

  • 快速的 net.tcp 绑定
  • Windows 凭据和身份验证
  • 加密和签名的传输(不允许嗅探数据包)
  • 简化的配置(隐藏所有我不想看到的内容)
  • Windows 服务宿主,在调试时行为类似于控制台应用程序
  • 服务的动态加载(添加新服务无需更改宿主代码)
  • 通用的客户端,因此我无需编写或生成代理代码
  • 真正实现 `IDisposable` 的客户端(为我隐藏 Abort 和 Close 的区别)
  • 调试模式下长超时,以便在调试时可以从容进行
  • 仅在调试模式下包含异常详细信息
  • 基服务类,带有一个简单的 `Authorize` 方法,以支持多个 Windows 组
  • 支持多个 Windows 组授权
  • 服务器和客户端具有相同的配置
  • 缓存服务插件服务和合同类型的解析
  • 解决方案中的程序集(项目)数量最少
  • 保持服务实现对客户端隐藏
  • (可能还有一些我忘记提及的)

Using the Code

定义了合同,实现了服务操作,并编写了少量的配置后,真正的乐趣就开始了:使用通用的客户端。您使用 `WcfServiceClient.Create` 方法,传入您想使用的服务的字符串键。然后,您只需使用返回对象的 `Instance` 属性即可使用服务的接口。

namespace WcfSvcTest
{
  class Program
  {
    static void Main(string[] args)
    {
      using (var client1 = WcfServiceClient<IThatOneService>.Create("test1"))
      {
        var name = client1.Instance.GetName("seed");
        Console.WriteLine(name);

        int addressId = 8;
        var address = client1.Instance.GetAddress(addressId);
        Console.WriteLine("{0}", address.City);
      }
 
      using (var client2 = WcfServiceClient<IMyOtherService>.Create("test2"))
      {
        try
        {
          Console.WriteLine(client2.Instance.GetName("newseed"));
          var per = client2.Instance.GetPerson(7);
          Console.WriteLine("{0}, {1}", per.Name, per.Title);
        }
        catch (FaultException<WcfServiceFault> fault)
        {
          Console.WriteLine(fault.ToString());
        }
        catch (Exception e) //handles exceptions not in wcf communication
        {
          Console.WriteLine(e.ToString());
        }
      }
 
      Console.ReadLine();
    }
  }
}

为节省空间,我在第二个测试服务上将客户端的使用包装在 `try catch` 中。

配置

这个通用的 WCF 服务宿主和客户端的关键部分之一是自定义配置节类 `WcfServiceConfigurationSection`。您可以下载代码进行查看,但此类允许我们为服务器和客户端定义一个单一、相同的配置节。这是配置了两个服务的简化配置。服务宿主和客户端的配置相同,大大简化了部署并减少了配置错误。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="wcfServices" 
          type="WcfServiceCommon.WcfServiceConfigurationSection, WcfServiceCommon" />
  </configSections>
  <appSettings/>
  <connectionStrings/>
  <wcfServices consoleMode="On">
    <services>
      <add key="test1"
          serviceAddressPort="localhost:2981"
          endpointName="Test1EndPoint"
          authorizedGroups="WcfServiceClients,someOtherGoup"
          hostType="Test1Service.ThatOneService, Test1Service"
          contractType="Test1Common.IThatOneService, Test1Common" />
      <add key="test2"
          serviceAddressPort="localhost:2981"
          endpointName="Test2EndPoint"
          authorizedGroups="WcfServiceClients,someOtherGoup"
          hostType="Test2Service.MyOtherService, Test2Service"
          contractType="Test2Common.IMyOtherService, Test2Common" />
    </services>
  </wcfServices>
</configuration>

安装 Windows 服务

您会注意到配置中的 `` 节点有一个名为“`consoleMode`”的属性,当值为“`On`”时,该属性指示通用宿主以控制台模式运行,否则作为 Windows 服务运行。这使得调试会话更容易。只需将您的 Visual Studio 解决方案设置为在客户端启动之前启动宿主服务,您就可以直接调试到您的实现。

要将服务安装为 Windows Server,您需要使用 .NET Framework 目录中的 `InstallUtil.exe`(例如,`C:\Windows\Microsoft.NET\Framework64\v4.0.30319`)。有关此实用程序的帮助,请参阅 MSDN 文章 installutil

服务合同和实现

这是简单的服务和数据合同

namespace Test1Common
{
  [ServiceContract]
  public interface IThatOneService
  {
    [OperationContract, FaultContract(typeof(WcfServiceFault))]
    string GetName(string seed);

    [OperationContract, FaultContract(typeof(WcfServiceFault))]
    Address GetAddress(int id);
  }

  [DataContract]
  public class Address
  {
    [DataMember]
    public string Line1 { get; set; }

    [DataMember]
    public string Line2 { get; set; }

    [DataMember]
    public string City { get; set; }

    [DataMember]
    public string State { get; set; }

    [DataMember]
    public string Zip { get; set; }
  }
}

这是服务的实现。它确实很简单,但请注意它继承自 `WcfServiceBase`(见下文)。

namespace Test1Service
{
  public class ThatOneService : WcfServiceBase, IThatOneService
  {
    public string GetName(string seed)
    {
      return "Mr. " + seed.ToUpper();
    }

    public Address GetAddress(int id)
    {
      return new Address 
      { 
        Line1 = "100 Main Street", 
        Line2 = "P.O. Box 100", 
        City = "MallTown", 
        State = "TX", 
        Zip = "12345" 
      };
    }
  }
}

WcfServiceBase 类

基类 `WcfServiceBase` 提供了服务实现所需的两个非常重要的通用需求。首先,您会得到一个 `ServiceBehavior` 属性,该属性在 DEBUG 生成时设置为 `IncludeExceptionDetailInFaults` 为 `true`。第二是 `Authorize` 方法,可以非常轻松地根据配置中的授权组列表对调用客户端进行授权。如果调用客户端在其中一个组中,`Authorize` 方法将不执行任何操作。否则,将抛出异常。

namespace WcfServiceCommon
{
#if(DEBUG)
  [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, 
   MaxItemsInObjectGraph = 131072, IncludeExceptionDetailInFaults = true)]
#else
  [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, 
   MaxItemsInObjectGraph = 131072, IncludeExceptionDetailInFaults = false)]
#endif
  public abstract class WcfServiceBase
  {
    public void Authorize()
    {
      string[] groups = null;
      Type serviceType = this.GetType();
      var configItem = Config.GetServiceConfig(serviceType);

      if (null != configItem)
      {
        groups = configItem.Item.AuthorizedGroups.Split(',');
      }

      if (null != groups)
      {
        PrincipalPermission[] pps = new PrincipalPermission[groups.Length];
        for (int i = 0; i < groups.Length; i++)
        {
          pps[i] = new PrincipalPermission(null, groups[i]);
        }

        PrincipalPermission pp = pps[0];
        if (groups.Length > 0)
        {
          for (int i = 1; i < groups.Length; i++)
          {
            pp = (PrincipalPermission)pp.Union(pps[i]);
          }
        }
        pp.Demand();
      }
      else
        throw new SecurityException("Group is null");
    }
  }
}

结论

我希望您觉得这个 WCF 插件架构和配套代码有用。我一定会使用它。如果您使用了它,请告诉我使用效果如何。

关注点

注意:您必须记住,WcfServiceHost 需要您动态加载的服务中的“common”和“service”程序集在其 `bin` 文件夹中。客户端(请参阅解决方案中的 WcfSvcTest 项目)也需要在其 `bin` 文件夹中包含“common”程序集的副本。您会发现我正在使用后置构建命令(`copy $(TargetPath) $(SolutionDir)WcfServiceHost\bin\debug\`)来实现这一点。当然,两者都需要具有相同的配置节,如代码所示。

更重要的注意事项:服务宿主项目 **不** 引用您的“common”或“service”实现。它们使用配置文件中的信息动态加载。您的客户端项目需要引用 WcfServiceCommon 程序集以及您的“common”合同程序集,当然,但 WcfServiceCommon 的通用 WcfServiceClient 将使用配置文件信息动态加载您的“common”合同程序集,因此 WcfServiceCommon 程序集 **不** 需要引用您的“common”合同程序集。

更新:关于进行异步调用

一位用户询问了异步使用的问题。下载 v2 源代码。留意更新后的 `WcfServiceClient` 类,关注 `AsyncBegin` 方法和 `AsyncCompleted` 事件。有关此功能的更通用版本,请查看我的新博客文章:通用异步包装器

历史

  • 2010/5/3 - 对文本进行了一些拼写更正,以及一两个语法和标题的修复。感谢我的审稿人和编辑 Charles W。
  • 2010/5/13 - 更新了 v2 源代码,其中包含了通用客户端的异步支持。
© . All rights reserved.