了解Microsoft.NET框架与COM+服务集成

时间:2021-05-15 03:12:23
COM 提供了一种编写基于组件的应用程序的方法。众所周知,编写 COM 组件需要进行大量重复的琐碎工作。而 COM+ 并不完全是 COM 的新版本,实际上,COM+ 为组件提供了一个服务基础结构。组件在构建后安装到 COM+ 应用程序中,可以建立易于部署、吞吐量高、可缩放的服务器应用程序。(如果组件不需要使用任何服务,则不应放到 COM+ 应用程序中。)为了达到可缩放性和吞吐量目标,需要从一开始就使用事务、对象池和活动语义等服务来设计应用程序。
  
  .NET 框架提供了另一种编写基于组件的应用程序的方法,与 COM 编程模型相比,它具有更好的工具支持、公共语言运行时 (CLR) 和更简单的编码语法等优势。COM+ 服务基础结构可以从托管和非托管代码进行访问。非托管代码中的服务称为 COM+ 服务。在 .NET 中,这些服务被称为企业服务。从 ServicedComponent 派生的类表明某个组件将需要服务。(如果组件不需要使用任何服务,则不应从 ServicedComponent 派生。)改进的工具支持使编程人员能够编写基于服务器的应用程序,而可缩放性和吞吐量问题仍需要通过良好的编程实践来实现。服务背后的基本理念是,从一开始就考虑吞吐量和可缩放性的设计,并利用企业服务在适当的位置轻松地实现那些设计模式。
  
  有人可能会提出异议,服务基础结构设计实际上与 COM 甚至组件都没有多大关系:现在,COM+ 服务可以用于 COM 组件、.NET 组件、甚至其他不能称为组件的实体,如 ASP 页或任意代码块等(请参阅 Microsoft Windows® XP 上的无组件服务 COM+ 功能)。
  
  今天,所有可用的 COM+ 服务都可以用于 .NET 和 COM 对象。其中一些服务包括:事务、对象池和构造字符串、JIT、同步、基于角色的安全性、CRM 和 BYOT 等。有关 Microsoft Windows 2000 上的服务的完整列表,请参阅 Platform SDK 中的“COM+ 提供的服务”。Microsoft Windows XP 包括 COM+ 的一个新版本,称为 COM+ 1.5,它具有一些附加服务,也可以用于 .NET 组件。
  
   事务
  为编写使用服务的托管应用程序,必须从 ServicedComponent 中派生需要服务的类,并使用各种自定义属性来指定所需的实际服务。本节介绍这些概念以及它们如何影响托管代码的编写。后面几节将进行详细说明。
  
  假设编写了一个 Account 类(其实际代码将在后面列出)并放置在 BankComponent 程序集中。则可以按照以下方法使用该类:
  
  BankComponent 客户端
  using system;
  using BankComponent;
  namespace BankComponentClient
  {
     class Client
     {
      public static int Main()
      {
       Account act = new Account();
       act.Post(5, 100);
       act.Dispose();
       return 0;
      }
     }
  }
  
  要建立该客户端,必须向 BankComponent 命名空间添加引用。此外,还必须为 System.EnterpriseServices 程序集添加引用 - 在 BankComponentClient 命名空间中,客户端将调用 Dispose() 和 ServicedComponent 构造函数,它们是在 System.EnterpriseServices 中定义的方法,而不是在包含 BankComponent 的程序集中定义的。当派生类没有重载所有基类方法时,.NET 通常要求使用这种处理方式。
  
  BankComponent 服务器代码显示了在 .NET 中使用事务的 Account 类的实现。Account 类是从 System.EnterpriseServices.ServicedComponent 类中派生的。Transaction 属性将该类标记为需要一个事务。由于使用了 Transaction 属性,所以将自动配置同步和 JIT 服务。AutoComplete 属性用于指定:如果在方法执行过程中出现未处理的异常,运行时必须为该事务自动调用 SetAbort 函数,否则,将调用 SetComplete 函数。ApplicationName 属性将此程序集与为此应用程序存储服务配置数据的 COM+ 应用程序关联起来。该类所需的进一步修改已在代码中标明。
  
  BankComponent 服务器
  using System.EnterpriseServices;
  [assembly: ApplicationName("BankComponent")]
  [assembly: AssemblyKeyFileAttribute("Demos.snk")]
  namespace BankComponentServer
  {
     [Transaction(TransactionOption.Required)]
     public class Account : ServicedComponent
     {
        [AutoComplete]
        public bool Post(int accountNum, double amount)
        {
        // 更新数据库,不必调用 SetComplete。
        // 如果没有出现异常,则自动调用 SetComplete。
        }
     }
  }
  
  从 BankComponent 服务器命名空间中的代码可以看出,在 .NET 中使用 COM+ 服务是很容易的。下面简单列出了从编码到部署的整个过程:
  
  编写服务器程序集。
  建立程序集:
  对程序集签名。可以为项目一次性地生成密钥文件,而不必为每次编译都生成密钥文件。要创建密钥,可以在 Microsoft .NET 命令提示下使用 sn.exe:
  sn –k Demos.snk
  
  编译代码。必须为 System.EnterpriseServices 添加引用。
  部署应用程序。
  必须在 COM+ 目录中注册使用受服务组件的程序集。ServicedComponent 类和自定义属性是从托管代码访问 COM+ 服务的两个关键概念。服务的配置存储在 COM+ 目录中。对象在 CLR 中驻留和执行。图 1 显示了托管对象及其相关联的 COM+ 上下文,在下面的两节中会更清楚。
  
 

  
图 1:与托管组件相关联的服务

  
  使用 COM+ 组件时需要手动配置目录,而使用受服务组件时,可以根据代码中的属性来更新目录。使用命令行工具 regsvcs.exe 或通过编写访问托管 API 的脚本可以显式注册程序集。后面的“部署”一节提供了详细的信息。在开发过程中,为方便起见,提供了 XCopy 部署,即简单地将程序集复制到应用程序目录中。每当客户端应用程序为从 ServicedComponent 中派生的类创建实例时,运行时都将检测是否已在 COM+ 应用程序中注册了该程序集。如果没有注册,则在本地目录中搜索程序集,如果找到了,该程序集中所有受服务组件都将在 COM+ 应用程序中注册,然后激活。这一过程称为迟缓注册,但并不适合所有情况。例如,标记为 COM+ 服务器应用程序的程序集需要显式注册(如下所示),迟缓注册不适合调用托管受服务组件的非托管客户端。迟缓注册在开发时很有用,因为如果没有它,就需要使用脚本、代码或 RegSvcs 来注册程序集。
  
  可以将程序集放到 GAC 中。有关详细信息,请参阅“部署”一节。
  运行客户端。
  
   部署
  自定义属性是从托管代码访问 COM+ 服务的两个关键概念之一。自定义属性用于指定所需的服务,如上述代码中的 Transaction 自定义属性。这些属性在程序集的元数据中存储了服务的配置选项。自定义属性的使用方式是:让某段代码加载程序集,然后使用反射来创建属性的实例并对其调用方法,从而提取存储在属性中的服务配置。然后,可以将该信息写入到 COM+ 目录中。执行这些步骤和其他步骤的代码包含在 EnterpriseServices.RegistrationHelper 中。为使注册过程更简单,所有注册窗体都使用了 EnterpriseServices.RegistrationHelper 组件。该组件可以作为托管类和 COM 对象来访问。
  
 

  
图" 2:注册受服务组件

  
  从概念上讲,RegistrationHelper 执行了以下步骤:
  
  使用 RegistrationServices.RegisterAssembly 在注册表中注册程序集。因此,在注册表中,类是作为以托管代码编写的 COM 组件出现的,并且具有指向 mscoree.dll 的 InprocServer32 键。如果托管类没有实现任何接口,则除非使用 ClassInterfaceAttribute,否则该类的公共方法不会出现在 COM+ 目录中。这意味着,与方法级相关联的服务配置不能存储在目录中。但是,某些 COM+ 服务可以在方法级上进行配置,并要求组件象在 COM+ 目录中显示的那样公开接口。例如,方法级上的 COM+ 基于角色的安全性要求组件实现一个接口以配置服务。此问题将在“安全性”一节中进行详细讨论。
  使用 TypeLibConverter.ConvertAssemblyToTypeLib 从程序集生成 COM 类型库。
  注册类型库。到目前为止,它与 RegAsm.exe /tlb 完全相同。
  查找或创建 COM+ 应用程序。其名称是从 ApplicationName 属性、程序集名称或所提供的应用程序的名称/GUID 中提取的。
  用类型库配置使用 COM+ admin API 的 COM+ 应用程序。
  检查所有自定义属性,并使用 IConfigurationAttribute 将特定服务的配置数据写入到 COM+ 目录中。
  RegistrationHelper 将试图使用 RegistrationHelperTx 在事务中执行这些步骤。RegistrationHelperTx 是在安装 .NET 时创建的 COM+ 应用程序中的一个类。因此,如果注册失败,COM+ 目录和注册表将恢复到其原始状态。但目前,生成的类型库将仍保留在磁盘上(或者在 GAC 中,如果程序集在 GAC 中)。如果正在注册的程序集引用了同样使用 COM+ 服务的其他程序集,则相关图中的所有程序集将执行上述相同的步骤。
  
  由于 RegistrationHelper 要访问 COM+ 目录,因而需要具有计算机上的非托管代码权限和管理权限。因此,对于 RegistrationHelper 的客户端也是一样,如迟缓注册、RegSvcs 或您的脚本/代码。这还意味着从 Internet 上下载的代码或存储在网络共享上的代码将不能进行注册。
  
  可以编写不兼容的属性组合,例如,请求一个 Transaction 并将 Synchronization 设置为禁用。当前,这些组合是在注册时检测的(当把它们写入到 COM+ 目录中时),而不是在编译时检测的。某些属性具有与其他属性的相关性,例如,当只使用 Transaction 属性时,相当于使用 Transaction、JustInTimeActivation 和 Synchronization 属性。注册托管组件时,如果不使用属性覆盖“未配置的”默认值,将使用 COM+ 目录的默认值。例如,如果注册一个组件并且没有指定 Transaction 属性,则目录中事务设置的未配置默认值将设置为 TransactionOption.Disabled。这种方法使开发人员在组件不再需要某个属性时,可以从代码中删除它,然后,当再次注册程序集时,再适当地重新设置事务的目录条目。联机文档中给出了这些未配置默认值的详细列表。默认的配置值是属性参数中的默认值,例如,只使用属性 [Transaction] 表示 TransactionOption.Required。
  
  由于托管类上的服务的配置数据存储在 COM+ 目录中,因此在注册程序集后,也可以通过管理的方式修改某些目录条目。但某些服务不能以这种方式修改。例如,在目录中禁用事务服务可能导致代码运行不正常。部署特有的设置(如对象构造字符串和安全性角色)可以在注册后进行处理。但如果在注册后设置,对于包含受服务组件的程序集的 XCopy 部署可能是不够的。COM+ 应用程序的导入和导出功能可以帮助分配应用程序的当前状态。有关导入和导出的进一步信息将在“远程组件”一节中介绍。
  
  在某些情况下,配置数据并不参考目录,而只是从程序集元数据中提取。这些情况包括自动完成、JIT、对象池(尽管池的大小是从目录中提取的)和安全方法属性。有关此问题的详细信息将在各个服务的相关章节中讨论。
  
  注册程序集的进程将自动生成 COM+ 所需的 GUID。如果程序集没有签名,则只根据类型和命名空间的名称生成 GUID。因此,如果程序集没有签名,则可能生成非唯一的 GUID。.NET 程序集可能遇到类似的情况,它甚至没有使用 COM+ 服务,但是需要唯一的类型名称。因此,必须对使用 COM+ 服务的程序集签名。如果程序集没有签名,注册将失败。注册还意味着使用 COM+ 服务的 .NET 类具有一个全局配置数据存储。尽管有可能将专用程序集复制到多个应用程序目录中,但最终所有这些应用程序都引用受服务组件的一个配置数据。因此,更改 COM+ 目录中的配置数据将影响使用该类的所有应用程序。这一点对于 Microsoft ASP.NET 配置中的多个 vroot 是很显然的,这些 vroot 都包括使用受服务组件的相同程序集的副本。使相同的 COM+ 应用程序具有多个配置的一种方法是在 Microsoft Windows .NET 上使用 COM+ 分区。要在 .NET 中使用 COM+ 分区服务,请不要使用 ApplicationID 属性 - 为在多个分区内安装相同的组件,COM+ 需要唯一的应用程序 ID。
  
  通常,当客户端需要访问不在客户端应用程序目录中的程序集时,或者如果程序集被加载到不在客户端目录中的其他进程中时,请使用 GAC。从概念上讲,使用受服务组件的专用程序集实际上是共享程序集 - 它们使用共享的配置数据。如果 ApplicationActivationOption 被设置为库,则有可能在程序集中的类上使用事务,并且,如果所有程序集都加载自同一个目录,则可以在一个客户端使用该程序集。当使用 ApplicationActivationOption 的程序集被设置为服务器时,该程序集将由 dllhost.exe(通常不在客户端目录中)加载。使用 COM+ 服务器应用程序中的受服务组件的程序集应放置在 GAC 中。使用 COM+ 库应用程序中的受服务组件的程序集则不必放置在 GAC 中(除非它们位于不同的目录)。唯一的例外是组件保留在 ASP.NET 中时 - 程序集不应放置到 GAC 中,这样才能使阴影副本能够正常运行。
  
  要删除使用受服务组件的 .NET 应用程序,应先从 GAC 中删除程序集(如果它是使用 GAC 注册的),使用 regsvcs.exe 从 COM+ 中取消程序集的注册,然后删除程序集及相关联的类型库。
  
   版本控制
  使用 GUID 属性可以确定 COM+ 所需的 GUID。但建议使用版本控制,而不是明确地使用 GUID。当创建新的方法签名,或当类具有不同的服务属性时,应递增程序集的主版本号或次版本号。每个版本都应进行一次注册。注册程序集的新版本时,将为该版本生成新的 GUID,而且组件将使用相同的组件名称在相同的 COM+ 应用程序中注册。因此,组件会在 COM+ 应用程序中多次出现。但每个组件都有由 GUID 给定的唯一 ID。每个实例都引用该组件的一个特定版本。当使用 Microsoft Visual Studio® .NET 建立 .NET 应用程序时,经常会遇到类似情况。环境将属性 [assembly:AssemblyVersion("1.0.*")] 添加到项目上。每次新建项目时都将生成新的内部版本号,因此,当重新注册程序集时,将生成新的 GUID。所以,最好在适当的时候手动递增内部版本号。客户端将使用 CLR 版本策略绑定到程序集上,从而能够使用 COM+ 应用程序中的类的正确版本。编写使用受服务组件的程序集(托管服务器)时的一些类似情况包括:(下面使用了激活的某些方面,这些内容将在下一节中介绍)
  
  托管客户端,托管服务器,在程序集中没有使用确定的 GUID。
  客户端将加载由版本策略指定的程序集。
  托管客户端,托管服务器,使用确定的 GUID。
  如果客户端激活了一个类并使用版本策略获取程序集的早期版本,则在激活过程中将使用代码中的确定的 GUID 从目录中提取服务信息。因此,来自使用此 GUID 的上次注册的程序集的信息将被用于创建对象,而实际上,该对象可能是一个新版本,因此,当试图从实际创建的对象 (v2) 向代码中的引用 (v1) 进行强制类型转换时,可能会出现类型转换异常。
  托管客户端,托管服务器,没有确定的 GUID,只更改内部版本号。
  尽管将生成新的 GUID,但由于类型库只有两个版本号,所以它将仍保留相同的版本号。但是如果版本 2 安装在版本 1 之上,则卸载版本 1 时,版本 2 的类型库将取消注册,从而无法工作。解决方案 1:.NET 框架的下一个版本 (V1.1) 通过启用独立于程序集的类型库版本控制,解决了这个问题。这意味着,更改程序集的版本号时,也应更改类型库的版本。解决方案 2:只使用主版本号和次版本号。
  非托管客户端,托管服务器,没有使用确定的 GUID。
  客户端将使用 GUID 创建组件。互操作性将把 GUID 解析为一个名称,然后应用版本策略。如果计算机上具有程序集的版本 1 和版本 2,并使用策略获取版本 2,则非托管客户端将获取版本 2。
  安装版本 1,安装版本 2,卸载版本 1。现在,除非版本策略重新指向版本 2,否则客户端将不能创建组件。此外,还必须存在包含版本 1 的注册信息的注册表项。要为卸载的版本 1 创建注册表信息,可以使用 Windows XP 上的 COM+ 别名功能。
  版本控制应用于同一 COM+ 应用程序中的所有组件,也就是说,不能为应用程序自动添加版本。例如,不能使用版本策略为应用程序上的角色添加版本。要为应用程序添加版本,请使用应用程序名称属性。
  
   受服务组件 激活
  企业服务基础结构是建立在上下文概念的基础之上的。上下文是具有类似执行要求的对象的环境。可以在激活和/或方法调用截取的过程中实施服务。尽管 COM+ 服务是用非托管代码编写的,但 COM+ 服务与 .NET 的集成要比仅在 .NET 中使用 COM 互操作技术强大得多。如果不从 ServicedComponent 中派生,注册进程将不会获得预期的效果。
  
  受服务组件可以用各种组合方式予以激活和保留。如图 3 所示,这里将讨论三种情况:进程内(相同的应用程序域)、应用程序域间(相同的进程)和进程间的激活。这些情况的重点在于调用组件时所跨越的边界。进程内激活可能会跨越上下文边界,在应用程序域间的情况下会跨越上下文边界和应用程序域的边界,而在进程间情况下将处理跨计算机、跨进程和跨上下文的边界。
  
 

  
图" 3:受服务组件的激活宿主

  
  受服务组件的实现依赖于 .NET Remoting,它为插入以非托管或托管代码编写的服务提供了可扩展的机制。受服务组件是从 ContextBoundObject 中派生的,并能实现诸如 IDisposable 等各种接口。使用 ProxyAttribute 派生的自定义属性可以轻易地自定义 CLR 中的激活链。通过编写自定义的真正代理可以自定义截取。当需要新的受服务组件派生类时,可以自定义激活链以使激活调用真正地调用 CoCreateInstance 的托管 C++ 包装程序。这将使 COM+ 能够根据存储在以前注册的程序集的 COM+ 目录中的信息设置非托管的上下文和服务。这也是实现迟缓注册的阶段。在程序集的注册过程中,InprocServer32 键指向 mscoree.dll,因此最终将 COM+ CreateInstance 重新定向到运行时,以创建真正的托管对象。因此,在激活过程中,将创建一个自定义的真正代理对象。此代理的进程内版本被称作受服务组件代理或 SCP,如图 4 所示。
  
 

  
图" 4:激活路径

  
  从激活调用返回的路径将封送托管代码中的托管引用,通过非托管的 COM+,返回到托管代码中(图 4 中线路 1 的相反路径)。根据真正对象的创建位置,客户端将引用拆封到相关窗体中。在进程内激活的情况下,引用被拆封为对透明代理 (TP) 的直接引用,如图 5 所示。应用程序域间的引用被拆封为 .NET Remoting 代理。进程间或计算机间的引用(图 6)需要更多的拆封处理:COM 互操作调用由 ServicedComponent 在激活和拆封过程中实现的 IManagedObject。远程受服务组件代理 (RSCP) 在激活过程中调用 IServicedComponentInfo 以获取服务器对象的 URI,这意味着在激活过程中进行了两次远程调用。当方法级上需要 COM+ 基于角色的安全性时,这些接口需要与一个角色相关联,以便在基础结构调用这些接口时成功地进行拆封处理。“安全性”一节将讨论进程间激活和拆封处理对配置基于角色的安全性的影响。
  
 

  
图" 5:进程内调用的基础结构

  
 

  
图" 6:进程外调用的基础结构

  
  因此,激活链已被自定义,以创建自定义的真正代理(用于截取)和创建非托管的上下文,只给 COM+ 留下了执行截取服务的语义所需的上下文基础结构。现在,COM+ 上下文与托管对象相关联,而不是与 COM 对象相关联。
  
  截取
  图 7 显示了进程内方法调用的基础结构。自定义代理 (SCP) 使得能够截取托管调用。在激活过程中,COM+ 上下文 ID 存储在 SCP 中。当一个托管对象调用受服务组件时,存储在目标 SCP 中的上下文 ID 将与当前的上下文 ID 进行比较。如果上下文 ID 相同,则直接在真正的对象上执行调用。如果上下文 ID 不同,SCP 将调用 COM+ 以切换上下文并呈现输入方法调用的服务。对于进程内的调用,与 AppDomain.DoCallBack 类似,只是 AppDomain 为 COM+。DoCallBack 函数首先进入 COM+(图 7 中的步骤 2),它将切换上下文并呈现服务,然后回调 SCP 上的函数调用。SCP 进行数据封送并在真正对象上调用方法。当方法退出时,返回路径允许 COM+ 呈现离开方法调用的语义(图 7 中的步骤 5)。COM+ 仅用于呈现服务。数据封送和方法调用是在 .NET 运行时中进行的,这样在调用方法时就不必进行类型转换(如从 String 转换到 BSTR)。如果进程内调用使用了 COM 互操作,则需要进行数据封送。因此,以非托管代码呈现服务的调用不是进程内调用的 COM 互操作调用。
  

  
图" 7:进程内调用的基础结构

  
  对静态方法的调用不转发到透明和真正的代理。因此,静态方法不能使用截取服务,它们是在客户端的上下文中被调用的。内部方法将在正确的上下文中被调用,这意味着,在为新事务配置的对象上调用内部方法的客户端将参与到新事务中。但是,由于方法级服务需要 COM+ 目录中的一个接口(在下一节和“安全性”一节中将详细介绍该主题),因此不能为方法级服务配置内部方法。服务可以应用到属性上,但方法级属性(如 AutoComplete)必须分别放在获得者和设置者方法上。
  
  AutoComplete 属性是使用事务的一种方便方法,无需编写任何代码以访问该服务。此外,还可以使用 ContextUtil.SetAbort 或 ContextUtil.SetComplete。该服务可以在 COM+ 资源管理器中通过设置方法属性的复选框进行配置。但托管对象不需要实现接口。受服务组件也是这样。如果接口上没有声明方法,方法级服务的配置将不能写入到注册的目录中,配置只能存储在元数据中。如果方法没有接口,上下文切换将从 SCP 中进行,使用存储在 IRemoteDispatch.RemoteDispatchAutoDone 上的配置信息(如果存在 AutoComplete 属性)。如果不存在 AutoComplete,则使用 IRemoteDispatch.RemoteDispatchNotAutoDone。IRemoteDispatch 是由 ServicedComponent 实现的一个接口。非托管客户端只能使用 IDispatch(后期绑定)调用没有接口的受服务组件,因此,由于在那种情况下没有真正的代理,因而不能实施 AutoComplete 语义。即使在使用接口时,AutoComplete 的配置仍然由托管客户端的元数据进行驱动。只有在进程外的情况下,才在 RemoteDispatchAutoDone 上进行 DCOM 方法调用。进程外组件不使用 DoCallBack 机制,而使用 DCOM 来发送调用和呈现服务。如果方法在接口上,则使用 DCOM 调用远程受服务组件上的接口方法,否则,调用将被调度到 ServicedComponent 上的 IRemoteDispatch 接口。这意味着即使象 Dispose() 这样的调用也是通过 DCOM 调用的,其含义将在以后讨论。
  
  上下文
  ContextUtil 类用于访问相关联的 COM+ 对象的上下文及其属性。它与由 CoGetObjectContext 以非托管代码方式返回的对象的功能类似。与受服务组件相关联的托管对象的上下文与相关联的非托管对象的上下文的用途不同。通过编写三个托管对象可以证明这一点,一个带有所需的事务(作为根),另外两个不从受服务组件中派生(作为上下文托管对象的子对象示例)。非受服务组件的行为将与带有事务支持的受服务组件一样,也就是说,它们可以调用资源管理器,而且必要时还可以使用 ContextUtil.SetAbort。创建根对象后,将创建相关联的非托管上下文并与当前的线程相关联。当调用子对象时,由于它们与非托管的上下文不相关,不需要进行 COM+ 上下文的更改,因而线程仍保持根的非托管上下文 ID。当子对象调用资源管理器时,资源管理器将从执行该子对象的线程中提取非托管上下文,即根对象的非托管上下文。依赖于这种方法是危险的,并且在以后的版本中,非托管上下文可能与托管上下文合并,因此,子对象将与潜在的、不同的托管上下文相关联,资源管理器将不再获得根对象的上下文。因此,升级到 .NET 的新版本可能会破坏依赖于这种行为的代码。
  
   性能结果
  在这一节中,将比较托管客户端、托管服务器服务的组件解决方案与非托管客户端/服务器解决方案的性能。下表介绍了进程内的情况。为事务配置的 ServicedComponent 是用 C# 编写的,带有简单添加数字的单一方法。用相应的 C++ 实现进行比较。这种比较在不做任何实际工作的情况下,显示出托管和非托管的解决方案之间的不同。在托管解决方案中,进程内激活的速度大约要慢上 3.5 倍,而且当有上下文切换时,方法调用的时间大约要多 2 倍。但是,当比较需要上下文切换的受服务组件方法调用和不需要上下文切换的受服务组件方法调用时,它们大约相差 3 个数量级,这表明进程内受服务组件截取基础结构是非常成功的。对于进程外解决方案,激活的时间大约多出 2 倍,上下文间的方法调用大约多出 3 倍。
  
  表 1 显示了使用托管和非托管解决方案时进程内激活和方法调用的时间比较。
  
  表 1:进程内激活和方法调用
  
  托管解决方案       非托管解决方案
     激活          35 10
  上下文间无操作方法调用    2 1
  上下文间有操作方法调用   200 100
  
  激活比无操作的方法调用的时间高一个数量级。加入一些工作以简单地获得一个 DTC 事务(但不对其做任何操作)可以使激活和方法调用的时间达到相同的数量级。当方法调用只简单地打开一个缓冲的数据库连接,其工作速度将比激活和无操作方法调用的结合高一个数量级,这就证明了当实验中加入实际工作时,受服务组件基础结构的系统开销只是理论值而已。
  
  对象的生存期 实时激活
  一般来讲,实时 (JIT) 服务不单独使用。它隐式地与事务服务一起使用,而且多数是与对象池一起使用。但是,此示例显示出一些有趣的主题。在以下代码中,.NET 类仅使用 JIT 服务编写。
  
  using System;
  using System.EnterpriseServices;
  [assembly: AssemblyKeyFile("Demos.snk")]
  [assembly: ApplicationName("JITDemo")]
  namespace Demos
  {
    [JustInTimeActivation]
    public class TestJIT : ServicedComponent
    {
      public TestJIT()
      { // 首先获得调用
      }
      [AutoComplete]
      public void DoWork ()
      {    // 用以下方法显示完成 ...
           // 1. autocomplete 属性或
           // 2. ContextUtil.DeactivateOnReturn = true 或
           // 3. ContextUtil.SetComplete();
      }
      public override void Dispose(bool b)
  {   // 有选择地替换此方法并使用您自己的
  // 自定义 Dispose 逻辑。如果 b==true,则从客户端调用 Dispose(),
  // 如果等于 false,GC 将清除对象
  }
    }
  }
  
  该类从 ServicedComponent 中派生,并使用 JIT 属性指明所需的特定服务。为重载非托管代码中的 Activate 和 Deactivate 方法,要求类实现 IObjectControl 接口。而 ServicedComponent 类则有虚方法,可被重载以处理 Activate 和 Deactivate 事件。但是,ServicedComponent 及其真正代理 SCP 都不能实现 IObjectControl。相反,当 COM+ 请求 IObjectControl 接口时,SCP 将创建一个代理剥离程序。然后,在剥离程序上的 COM+ 的调用被发送到 ServicedComponent 的虚方法中。DeactivateOnReturn 位是通过使用方法上的 AutoComplete 属性、调用 ContextUtil.SetComplete()、ContextUtil.SetAbort() 或设置 ContextUtil.DeactivateOnReturn 来设置的。假定 DeactivateOnReturn 位是在每个方法调用的过程中设置的,则方法调用的顺序将为:类的构造函数、Activate、实际方法调用、Deactivate、Dispose(true),最后是类的终结器(如果存在)。进行另一个方法调用时,将重复相同的顺序。一个好的设计只需重载 Activate 和 Deactivate 方法即可知道对象何时被取出,何时又放回对象池。Activate 和 Deactivate 的其他逻辑应放置在类的构造函数和 Dispose(bool) 方法中。可以使用以下方法之一设置 DeactivateOnReturn 位:
  
  客户端使用对象状态只进行单一的方法调用。在进入方法时,将创建一个新的真正对象并附加到 SCP 上。在退出方法时,将首先调用 Dispose(true),然后调用真正对象的终结器(如果存在),以此停用真正对象。但是,相关的 COM+ 上下文、SCP 和 TP 仍然存在。客户端代码将仍然保持引用它所认为的实际对象(透明代理)。客户端对同一个引用所做的下一个方法调用将创建新的真正对象并附加到 SCP 上,以便为方法调用提供服务(请参阅“对象池”一节以删除创建新对象的要求)。要停用真正对象,当方法调用退出时,真正对象需要表明已完成。这可以通过使用以下方法实现:
  
   类的方法上的 AutoComplete 属性
  ContextUtil 类上的两个方法调用之一,DeactivateOnReturn 或 SetComplete
  客户端对相同的对象进行多次方法调用,每次方法调用后并没有停用对象(通过在退出方法之前将完成位设置为 false)。例如,规范在窗体级上使用 JIT 的受服务组件,并让两个窗体按钮在相同的对象实例上调用方法(通过让方法显式地将完成位设置为 false)。在某些方面,完成位应设置为 true。这种方法意味着客户端和对象之间存在着联系。客户端可以隐式或显式地完成这一内容:
  客户端知道当对象完成时在对象上调用某种方法以停用该对象。该方法的实现使用了选项 1 中的概念。还可以使用相同的调用顺序调用对象引用,这意味着将创建新的真正对象。
  当客户端在对象上调用 Dispose() 方法时,对象将被显式销毁。Dispose() 是在 ServicedComponent 上定义的方法,它依次调用 Dispose(true)、类的终结器(如果存在),然后剥离相关的 COM+ 上下文。在这种情况下,在对象引用上不能再进行进一步的方法调用。如果仍试图调用,将引发一个异常。如果很多客户端都使用相同的对象,则只有当最后一个客户端使用完该对象时才能调用 Dispose()。但是,JIT 对象的无状态特性导致这样一种设计模式,即每个客户端一个实例。
  对象从不将其完成位设置为 true,客户端也从不调用 Dispose()。当发生垃圾回收时,真正对象、代理和上下文将被销毁。由 GC 启动的方法调用顺序将为 Deactivate、Dispose(false) 和类的终结器(如果存在)。
  所有受服务组件都有相关联的 COM+ 上下文,它作为引用存储在 SCP(在远程情况下,则为 RSCP)中。只有当发生 GC 或客户端调用 Dispose() 时,才释放该引用。最好不要依赖 GC 来清除上下文:COM+ 上下文束缚在一个 OS 句柄和一些内存上,从而可能会延迟这些句柄的释放,直至发生 GC。而且,尽管 ServicedComponent 没有终结器,但 SCP 实现了一个终结器,这意味着 COM+ 的上下文引用永远不能在第一次回收时被回收。实际上,当最终调用 SCP 上的终结器时,上下文还没有被终结器线程销毁,相反,销毁上下文的工作已经从终结器中删除,而被放到了内部队列中。这样做是因为人们发现,在某种压力环境中工作时(即受服务组件被快速创建、使用和放到范围之外),终结器线程会消耗大量资源。因而使用内部线程为队列提供服务,销毁旧的上下文。此外,任何创建新的 ServicedComponent 的应用程序线程都将首先试图从队列中取出一个项目并销毁旧的上下文。因此,从客户端调用 Dispose() 将使用客户端线程更快地剥离 COM+ 上下文,而且,它将释放上下文所消耗的句柄和内存资源。有时 Dispose() 可能会引发异常。一种情况是,如果对象驻留在已经终止的非根事务上下文中,则 Dispose() 调用可能会引发 CONTEXT_E_ABORTED 异常。另一种情况将在对象池中说明。
  
  从性能角度来看,最好不要在 ServicedComponent 的派生类中实现终结器,而将此逻辑放到 Dispose(bool) 方法中。尽管 SCP 实现了终结器,但真正对象的终结器是使用反射调用的。
  
  使用 JIT 的一个较好的设计方法为:
  
  在构造函数和 Dispose(bool) 方法中放置自定义的激活和终结代码,不要通过在方法上使用 AutoComplete 属性来指明完成以实现终结器和使用单一的调用方式。
  当客户端处理完对象时,从客户端调用 Dispose()。
  这里假定客户端是托管的,并且组件是进程内的。当组件在进程外时:(在“远程组件”一节中将详细介绍)
  
  仅当客户端激活对象的 .NET Remoting 租约时间过期时,GC 才清除对象。
  如前面所述,当在进程外组件上调用方法时,将使用 DCOM 来切换上下文并发送方法调用。如果组件先由 JIT 停用,然后再调用 Dispose(),将进入服务器的上下文,而且将重新创建真正对象,以便为 DCOM 提供服务并最终再次停用。对于进程内组件,如果真正对象已经停用,则在为 Dispose() 调用(它将重新激活该组件)提供服务之前,不会试图切换到正确的上下文,而只是销毁了上下文。
  
   对象池
  对象池的基本用途是对象的重用。对象池通常与 JIT 一起使用。缓冲的 COM 组件和 .NET 组件都是如此。
  
  using System;
  using System.EnterpriseServices;
  [assembly: AssemblyKeyFile("Demos.snk")]
  [assembly: ApplicationName("OPDemo")]
  namespace Demos
  {
  [ObjectPooling(MinPoolSize=2, MaxPoolSize=50, CreationTimeOut=20)]
  [JustInTimeActivation]
  public class DbAccount : ServicedComponent
  {
    [AutoComplete]
    public bool Perform ()
    {   // 进行一些操作
    }
    public override void Activate()
    {  // .. 处理 Activate 消息
    }
    public override void Deactivate()
    {  // .. 处理 Deactivate 消息
    }
    public override bool CanBePooled()
    { // .. 处理 CanBePooled 消息
     // 基本实现返回 false
     return true;
    }
  }
  }
  
  正如使用 JIT 的情况,可以通过两种方法使用对象池:
  
  单一调用模式。在代码中,假定将 JIT 用于对象池,并且在方法调用中将完成位设置为 true,则当客户端试图进行方法调用时,将从池中提取对象,并在从此单一的方法调用中退出时返回池中。使用 JIT 的相同的单一调用方法也适用于此处。仅在创建对象并将其放到池中时才调用一次构造函数。使用 JIT 和缓冲对象的方法调用的顺序为:Activate、方法调用、Deactivate 和 CanBePooled。如果 CanBePooled 返回 true,则对象将放回到池中(尽管如前面所述,上下文仍然存在)。从池中任意提取一个对象后,随后的方法调用(不再调用构造函数)将重复相同的顺序(受服务组件不能使用参数化的构造函数)。最后,如果客户端在缓冲对象上调用 Dispose(),则在进程内的情况下,只有上下文被销毁。在进程外的情况下,如上所述,对 Dispose() 的调用将重新激活对象。如果对象被缓冲,则必须从池中获得一个对象,这意味着 Dispose() 能够引发一个 CO_E_ACTIVATION_TIMEOUT 异常。
  多次调用模式。使用类似于 JIT 服务中说明的多次方法调用,只有当对对象进行多次方法调用后才能将对象放回到池中。但是,如果客户端没有调用 Dispose,并且也没有使用 JIT,则无法确保当对象由 GC 放回到池中后,需要终结的缓冲对象的子对象还能够重新使用。当对缓冲对象进行回收处理时,在 Deactivate 中也不能保证哪个成员仍然有效。在 .NET 框架的下一个版本 (V1.1) 中,没有调用 canBePooled 和 Deactivate,而且也没有将对象放回到池中。这种方法有一个更一致的模式 - 在 Deactivate 中,子对象仍存在,而在 Dispose() 中,则不能保证子对象的存在。因此,为不使用 JIT 的缓冲对象调用 Dispose() 是很重要的,否则对象将不能返回到池中。
  管理员可以在程序集部署和注册后修改池的大小和超时。池大小的变化将在重新启动进程时生效。在 Windows XP 或更好的版本中,池的大小应用到进程中的每个应用程序域中。在 Windows 2000 中,池的大小是在驻留在默认应用程序域中的缓冲对象的进程范围内有效的,这意味着,如果相同进程中的另一个应用程序域需要缓冲对象,客户端能够有效地跨过应用程序域以获取缓冲对象。要实现这一点,可以从 ASP.NET 应用程序中(其中每个 IIS vroot 都驻留在独立的应用程序域中)使用在 COM+ 库应用程序中定义的缓冲的 .NET 对象。
  
  受服务组件不能使用参数化的构造函数。
  
  安全性 代码访问安全性 (CAS)
  .NET 框架的安全性促使代码仅在具有访问权限时才能访问资源。为了表示这一点,.NET 框架使用了权限的概念,它代表代码访问受保护资源的权力。代码请求所需的权限。.NET 框架提供代码访问权限类。也可以编写自定义的权限类。这些权限可用于向 .NET 框架指明代码需要获得的操作许可以及代码的调用者必须获得的操作授权。任何通过 System.EnterpriseServices 的代码路径都需要请求非托管的代码权限。
  
  如果代码是从网上下载的,并且不能完全信任作者,那么 .NET 中的代码访问安全性在这样的应用程序中会非常有用。通常,使用受服务组件的应用程序是可以完全信任的,它需要安全性以便在多个进程间流动,并在部署时启用角色配置。这些是由 COM+ 基于角色的安全性提供的功能。
  
  任何通过 System.EnterpriseServices 的代码路径都需要请求非托管的代码权限。这意味着:
  
  对受服务组件进行激活和执行上下文间的调用时要求具有非托管的代码权限。
  如果将对受服务组件的引用传递到不信任的代码,则在 ServicedComponent 上定义的方法将不能从不信任的代码调用。但在某些情况下,在从 ServicedComponent 派生的类上定义的自定义方法可以从不信任的代码调用。如果自定义方法不要求上下文切换、截取服务,并且方法的实现不会调用 System.EnterpriseServices 的成员,则可以从不信任的代码调用这些方法。
  此外,在 .NET 版本 1 中,切换线程时不复制安全性堆栈,因此不应在受服务组件中使用自定义的安全性权限。
  
  基于角色的安全性 (RBS)
  System.EnterpriseServices 为 .NET 对象提供了采用 COM+ 安全性机制功能的安全性服务。当使用 COM+ 服务器应用程序保留组件时,RBS 功能要求使用 DCOM 传输协议从远程客户端激活组件。下一节将介绍有关远程的详细信息。因此,COM+ 中的安全性调用上下文和标识可以用于托管代码。此外,CoImpersonateClient、CoInitializeSecurity 和 CoRevertClient 是通常用在服务器端的常用调用,而 CoSetProxyBlanket 通常用在客户端。
  
  某些安全性设置并不是使用属性存储在元数据中的,例如,向角色添加用户和设置进程安全性标识等。但是,程序集级的属性可用于配置 COM+ 服务器应用程序在 COM+ 资源管理器中的安全性选项卡中的内容:
  
  启用应用程序的授权 (ApplicationAccessControlAttribute(bool))。必须为 true 以支持 RBS。
  安全性级别 (ApplicationAccessControlAttribute(AccessChecksLevelOption))。如果设置为 AccessChecksLevelOption.Application,在应用程序中指定给角色的用户将添加到进程安全性描述符中,并将关闭对组件、方法和接口级的细致检查。因此,安全性检查只在应用程序级进行,并且库应用程序的进程级安全性依赖于宿主进程。如果属性设置为 AccessChecksLevelOption.ApplicationComponent,则在应用程序中指定给角色的用户将添加到进程安全性描述符中,并在应用程序上执行基于角色的安全性检查。此外,还要通过在类上应用 ComponentAccessControl 属性来为每个需要 RBS 的组件启用访问权限检查。在库应用程序中,基于角色的安全性检查象在服务器应用程序中那样执行。安全性属性包括在应用程序中所有对象的上下文中,并可以使用安全性调用上下文。如果对象的配置与其创建者的上下文不兼容,它将在自己的上下文中激活。程序化的基于角色的安全性依赖于安全性调用上下文的可用性。
  要使用于 COM+ 库应用程序的任何访问权限检查有意义,请选择在进程级和组件级执行访问权限检查。
  
  impersonation 和 authentication 选项对应于 ApplicationAccessControl 属性中的 ImpersonationLevel 和 Authentication 属性。
  可以在程序集级、类级或方法级上应用 SecurityRole 属性。当应用到程序集级时,角色中的用户可以激活应用程序中的任何组件。当应用到类级时,角色中的用户还可以在组件上调用任何方法。应用程序级和类级的角色可以在元数据中配置,或通过访问 COM+ 目录以管理的方式进行配置。
  
  使用元数据在程序集级上配置 RBS:
  
  [assembly: ApplicationAccessControl(true,
  AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)]
  // 向此角色添加 NTAuthority/everyone
  [assembly:SecurityRole("TestRole1",true)]
  // 以管理的方式向角色添加用户
  [assembly:SecurityRole("TestRole2")]
  
  使用元数据在类级上配置 RBS:
  
  [assembly: ApplicationAccessControl(true,
  AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)]
  ?[ComponentAccessControl()]
  [SecurityRole("TestRole2")]
  public class Foo : ServicedComponent
  {
  public void Method1() {}
  }
  
  在程序集级或类级上的 RBS 可以通过管理的方式进行配置,因为在注册程序集后,那些实体将存在于 COM+ 目录中。但是,如前面所述,类方法并不出现在 COM+ 目录中。要在方法上配置 RBS,该类必须实现接口的方法,并且必须在类级上使用 SecureMethod 属性,或在方法级上使用 SecureMethod 或 SecurityRole 属性。此外,属性必须出现在类方法的实现中,而不是出现在接口定义的接口方法中。
  
  在方法上使用 RBS 的最简单的方法是在类级上应用 SecureMethod 属性,然后配置角色(可以通过管理的方式,或通过在方法上放置 SecurityRole 属性)。
  [assembly: ApplicationAccessControl(true,
  AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)]
  Interface IFoo
  {
  void Method1();
  void Method2();
  }
  [ComponentAccessControl()]
  [SecureMethod]
  public class Foo : ServicedComponent, IFoo
  {
  // 以管理的方式向此方法添加角色
  public void Method1() {}
  // “RoleX”被添加到此方法的目录中
  SecurityRole("RoleX")
  public void Method2() {}
  }
  
  在类级上使用 SecureMethod 可以使该类中所有接口上的所有方法都使用 COM+ 目录中的角色以管理的方式进行配置。如果该类实现了两个具有相同方法名称的接口,并且角色是以管理的方式配置的,那么必须在这两种方法出现在 COM+ 目录中时对它们进行角色配置(除非该类实现了特定的方法,如 IFoo.Method1)。但是,如果在类方法上使用了 SecurityRole 属性,则当注册程序集时,所有同名的方法都将自动使用该角色进行配置。
  
  也可以将 SecureMethod 属性放到方法级上。
  [assembly: ApplicationAccessControl(true,
  AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)]
  Interface IFoo
  {
    void Method1();
    void Method2();
  }
  [ComponentAccessControl()]
  public class Foo : ServicedComponent, IFoo
  {
  // 以管理的方式向此方法添加角色
  [SecureMethod] // 或使用 SecurityRole(转换为 SecureMethod++)
    public void Method1() {}
    public void Method2() {}
  }
  
  在此示例中,IFoo 和两种方法都出现在 COM+ 目录中,因此可以在每种方法上以管理的方式配置角色,但是,方法级 RBS 只能在 Method1 上实施。将 SecureMethod 或 SecurityRole 用于所有方法(将被要求参与方法级 RBS 安全性)上,或者如前面所述,将 SecureMethod 放置到类级上。
  
  只要在方法级上配置 RBS,就需要 Marshaller 角色:当进行方法调用而在方法上没有配置 RBS 时,受服务组件基础结构将调用 IRemoteDispatch。当进行方法调用且在方法上配置了 RBS 时(当存在 SecureMethod 属性时),则使用 DCOM(使用与方法相关联的接口)进行方法调用。因此,DCOM 将保证在方法级上实施 RBS。但是,如“激活”和“截取”各节中所述,然后,COM 互操作和 RSCP 将调用 IManagedObject(以便让远程激活器将引用封送到其空间内)和 IServicedComponentInfo(以便查询远程对象)。这些接口与受服务组件相关联。由于组件被配置为进行方法级检查,因此,如果基础结构要成功进行这些调用,将需要一个角色与这些接口相关联。
  
  因此,当注册程序集时,向应用程序添加了一个 Marshaller 角色,然后,必须以管理的方式向该角色添加用户。通常,应用程序的所有用户都添加到该角色中。这与非托管 COM+ 有些不同,在非托管 COM+ 中,在方法上配置 RBS 不需要此附加的配置步骤。在注册时自动向该角色添加“Everyone”是一个潜在的安全漏洞,因为现在每个人都可以激活(但不能调用)组件,而在此之前,他们可能无权激活它们。Marshaller 角色还将添加到 IDisposable 接口,以使客户端能够清除该对象。Marshaller 角色的一种替代方法是用户向所提到的三个接口中的每个接口添加相关角色。
  
   远程组件
  ServicedComponent 类在其继承树中包含了 MarshalByRefObject,因此可以从远程客户端进行访问。有很多方法可以远程公开受服务组件。可以使用以下方法远程访问受服务组件:
  
  HTTP 通道,其中受服务组件从 ASP.NET 中调用,或在其中编写。这种方法提供了良好的安全性和加密选项,此外还有众所周知的可缩放性和性能。如果与 SOAP 结合使用,还会有更多的互操作选项。受服务组件可以作为 COM+ 库应用程序保留在 IIS/ASP.NET 中。如果使用 COM+ 服务器应用程序,IIS/ASP.NET 宿主可以使用 DCOM 访问这些组件。
  将受服务组件公开为 SOAP 端点的另一种方法将在 COM+ Web 服务:通过复选框路由到 XML Web 服务中讨论。
  当受服务组件保留在 Dllhost 中时使用 DCOM。该选项提供了优化的性能和安全性,并提供了在计算机间传递服务上下文的能力。选择远程技术时,最大的设计问题应该是服务是否需要在计算机间流动。例如,在一个服务器领域中,在一台计算机上创建了一个事务,并要求该事务在另一台计算机上继续,为此,只能使用 DCOM 协议。但是,如果客户端只需调用一个远程 ServicedComponent,则 HTTP 通道或 SOAP 端点方法将是很好的替代方法。
  .NET Remoting 通道(例如,TCP 或自定义的通道)。要使用 TCP 通道,需要一个侦听套接字的进程。通常,使用一个自定义进程来侦听套接字,然后将受服务组件作为 COM+ 库或服务器应用程序予以保留。或者,也可以使用 Dllhost 进行侦听。但这两种方法都不太可能使用,并且需要编写自定义的套接字侦听程序(其性能、可缩放性和安全性应已被证明)。因此,ASP.NET 或 DCOM 解决方案是大多数项目的最佳方法。
  为了使用 DCOM 远程访问受服务组件并将其保留在 Dllhost 中,首先要确保在 COM+ 服务器应用程序中注册程序集,并把它放在服务器计算机上的 GAC 中。然后,使用 COM+ 应用程序导出功能为应用程序代理创建一个 MSI 文件。在客户端上安装应用程序代理。托管程序集嵌入在应用程序代理中。安装程序也将注册该程序集并将它放到客户端计算机上的 GAC 中。因此:
  
  需要在客户端和服务器上安装 .NET 框架。即使只有非托管客户端将访问远程受服务组件,也需要在客户端计算机上安装 .NET 框架。在 Windows 2000 平台上,还需要安装 Service Pack 3。
  卸载代理后,也必须从 GAC 中删除程序集。
  从客户端的托管代码中激活服务器组件后,其基础结构如 图 6 所示。
  
  使用 DCOM 意味着 CLR 保留在 Dllhost 中,也就是说,应用程序配置文件 dllhost.exe.config 驻留在 system32 目录中。这也意味着配置文件将应用于计算机上的所有 Dllhost 进程。在 .NET 框架的下一个版本 (V1.1) 中,可以在 COM+ 应用程序中设置 COM+ 应用程序的根目录,该目录用于为应用程序查找配置文件和程序集。
  
  对于客户端激活的对象,每当请求对象的 URI 时,即为该对象创建一个生存期租约。如前面“激活”一节中所述,URI 是由远程受服务组件代理请求的。当现有的进程内受服务组件被封送到远程进程时也会出现这种情况 - 每当 MBR 对象被 .NET 封送到应用程序域以外时,都要请求 URI。URI 用于确保 .NET 中的对象标识是唯一的,并防止出现代理链。因此,当托管客户端激活一个远程受服务组件时,将对服务器对象使用租约时间。请注意,非托管客户端在客户端没有远程受服务组件代理,因此不请求对象的 URI。它使用 DCOM 来确保对象的标识。因此,从非托管客户端激活受服务组件时不对其使用租约时间。
  
  当受服务组件带有租约时间时,最好将 InitialLeaseTime 和 RenewOnCallTime 超时值设置为一个较小的值,甚至可以为 10 秒。可以使用 Dispose() 或让 GC 清除对象以销毁受服务组件。当调用 Dispose() 时,远程受服务组件代理将释放在 DCOM 代理上的引用,然后使自己可用于下一个 GC。服务器对象将处理 Dispose 调用(或创建一个新的服务器对象以便为远程调用提供 Dispose() 服务),销毁相关联的 COM+ 上下文,然后使自己可用于下一个 GC,但只有当超过租约时间时才可以使用。当客户端没有调用 Dispose() 时,服务器将首先等待客户端的 GC 释放对 DCOM 代理的引用,然后在超过租约时间时,才能使自己和 COM+ 上下文可用于下一个 GC。因此,要调用 Dispose(),并且还要降低默认的租约时间。即使客户端仍然存在,而租约时间已过期,对服务器对象的 DCOM 引用将使服务器对象继续生存。但是,不能总使用 DCOM 引用以维持受服务组件的生存状态。当客户端通过 CLR 远程通道或 COM+ SOAP 服务访问对象时,只有与租约有关的强大引用才能使受服务组件继续生存。
  
   小结
  本文只讨论了可用于托管代码的一些服务。所有 COM+ 服务都可用于托管代码,如事务隔离级别、进程初始化、无组件的服务以及进程循环等。现在,.NET 框架以一致且具有逻辑性的方式提供了对所有 COM+ 服务的同等访问。而且,.NET 框架的许多创新部分,如 ASP.NET、Microsoft ADO.NET 和消息传递等,与 .NET 企业服务紧密集成在一起,从而可以利用诸如事务和对象池等服务。这种集成提供了一致的体系结构和编程模型。System.EnterpriseServices 命名空间提供了向托管类添加服务的编程模型。