使用 EnterpriseServices 在 .NET 中创建 COM+ 对象






4.74/5 (34投票s)
2003 年 3 月 24 日
14分钟阅读

274366

3180
一篇关于使用 .NET 创建涉及跨多个数据库事务处理的 COM+ 分布式组件的文章。
引言
好的,这是我在 CodeProject 上的第一篇文章,但我希望它不太明显。我花了很长时间,一路想出了许多我认为不错的文章想法,但由于我的性格,我从未真正开始写,因为我花了很多时间在休息室里。废话少说,我只希望您喜欢并发现以下内容很有用。
背景
我一直想在更具挑战性的环境中掌握 .NET,即需要在多台机器之间分发工作负载,同时又不能让数据受到丝毫损坏。根据我的 C++ 经验,我知道这需要构建 ATL COM+ 组件,并实现各种接口。使用 .NET Enterprise Services 似乎有一种更简单的方法,我认为它没有得到充分讨论,而且 MSDN 文档也有所欠缺。因此,我的目标是快速介绍如何构建一个示例分布式数据库事务,并深入探讨一些细节。我猜想我遗漏了一些解释,所以请告诉我,我会尝试扩展。
Windows 上分布式事务简史
我假设您熟悉典型的“开始”、“提交”和“回滚”事务周期的数据库事务。例如:
Sub UpdateDB()
Dim cnn As New ADODB.Connection
Dim rs As New ADODB.Recordset
cnn.Open "Northwind", "Admin", "Password"
cnn.BeginTrans
rs.Open "tblOrders", cnn, adOpenDynamic, adLockOptimistic
'
' Do something to the database
'
rs.Close
cnn.CommitTrans
cnn.Close
Exit Sub
err_handler:
cnn.RollbackTrans
cnn.Close
End Sub
图 1 - 显示 ADO 示例 - 请原谅使用 Visual Basic,但这是一个快速示例
在 Windows NT4 时代,分布式事务协调器 (DTC) 作为 SQL Server 6.5 的一部分被引入。之后,微软事务服务器 (MTS) 被引入,它包含了 DTC,提供了中间层服务器端 COM 对象组件在负载均衡的分布式系统中存在的可能性。这些组件可以参与事务,并对它们是否成功完成了作为事务中应用程序一部分的任务进行投票。开发人员创建的 MTS 组件可以远程管理,并具备为每个 MTS COM 对象设置接口级别的 NT 用户安全权限等功能。
后来在 Windows 2000 中,MTS 发展为 COM+。COM+ 的优点是安全性更加精细,管理员可以授予/撤销特定用户对每个 MTS COM 对象接口级别方法的访问权限。以前,用户不得不实现额外的安全措施的情况并不少见,因为权限级别不够详细。从现在开始,我将假设您针对的是 Windows 2000 系统或更高版本,并将其称为 COM+。
随着 .NET 的出现,如果要在企业环境中使用它,那么它就需要稳定、容错和安全。那么,为什么不在已经非常优秀的 COM+ 框架之上添加这些功能呢?EnterpriseService
命名空间(默认情况下需要添加引用)包含了您执行分布式事务所需的一切。这里唯一需要注意的是,.NET COM 互操作需要尽可能干净地处理,这很好。用 C++ 和 VB 编写 COM+ 需要在类中实现相当多的接口。在 .NET 中,您只需从 ServicedComponent 对象派生您的类,这可以省去很多繁琐的工作。
为什么要进行分布式事务?
在当今大型且要求苛刻的业务应用程序中,从数据库中提取数据,根据业务规则对其进行复杂计算,最后更新各种记录,这些工作量对于一台机器来说可能过于庞大。此外,即使是较小的应用程序,从灾难恢复的角度来看,将其托管在用户的工作站上也会让大多数 IT 管理员感到担忧。COM+ 提供了一个可伸缩、容错的平台,可以创建中间层 COM 对象,服务器或服务器集群可以共同确定该对象在网络上的位置。
注意:Windows 2003 Server 在容错方面得到了极大的增强,它拥有 8 节点群集和故障转移,并且可以承受 3 台机器发生故障,而事务仍然不受阻碍地进行。
此外,COM+ 还解决了另一个问题。当您需要作为事务的一部分修改位于不同服务器上的两个或多个不同数据库时,会发生什么?例如:
您有一个人事数据库和一个销售/订单数据库。它们是分开实现的,因为需要将员工的个人数据(如薪金和奖金信息)与大多数员工用于订单处理的日常业务数据库完全分开。或者从遗留的角度来看,它就是这样构建的,而且他们不允许您更改它(听起来是不是很熟悉)。
现在,您作为开发人员,被要求实现一个可靠的功能,作为添加和删除员工到两个数据库的应用程序的一部分,这需要作为事务的一部分来完成——要么两个数据库都更新,要么在出现问题时都不更新。
但我听到您在尖叫,如果我使用的是 ADO 连接对象,它一次只能连接到一个数据库,并且有自己的“开始”、“提交”和“回滚”事务方法。我需要运行两个连接,并自己处理看起来很混乱的实现吗?
您很高兴听到的答案是“否”。在 COM+ 下,您甚至不需要调用连接的“开始”、“提交/回滚”,因为它们已经知道它们是在事务上下文中运行的,DTC 已经通知了它们。因此,从某种角度来说,需要编写的代码也更少。
这一切都依赖于这样一个事实:在事务层下方是一个可以由 DTC 指令的数据库平台。SQL Server、Oracle 和 DB2 都能很好地配合。然而,Sybase 则不行,因为它对此一无所知。
在 .NET 中实现一个示例 COM+ 对象
我将向您展示如何实现一组非常简单的对象,这些对象可以在事务中工作,并作为事务的一部分,其中一个对象将是启动新事务上下文的根业务对象。
需要新事务
• 员工维护(从客户端应用程序的角度来看,是根对象)
需要现有事务,或创建一个新事务(如果直接由客户端调用)
• 工资维护
• 订单维护
我不会详细介绍如何管理 COM+ 应用程序的安全(也许以后会写一篇),并且在此示例中将假定所有内容都在同一个用户帐户下运行,因此不会引起任何问题。尽管如此,配置起来也很容易。在 Windows 2000 或 XP 中,转到“控制面板”->“管理工具”->“组件服务”(参见图 6)。这就是 COM+ 管理器。打开本地计算机,然后浏览到包含 COM 对象的本地 COM+ 应用程序包。在这些包中,您可以查看它们实现的 COM 接口以及这些组件上的方法。
示例应用程序的基本布局
以下是我尝试绘制系统布局图的尝试。
图 2 - 基本实现布局
我们将构建一个简单的胖客户端 WinForm 应用程序,允许用户在单个事务中在两个数据库中添加用户。编写一个使用此中间层 COM+ 对象的基于 Web 的管理页面也很容易,但这可能留到另一篇文章。
对于新员工,公司要求我们存储
• 姓名
• 地址
• 工作类型
是的,不多,但足以作为示例。人事部和订单部数据库都需要知道员工的姓名和工作类型,但只有人事部需要知道员工的家庭住址以便邮寄工资单。
此时,您应该在 Visual Studio .NET 中创建一个空白解决方案,并给它起一个朗朗上口的名字 MyBusinessSolution。向其中添加三个 C# 类库:
• Administration
• Personnel
• Orders
我这样布局是为了在以后可能添加其他组件来完成比设置员工更多的工作。毕竟,公司不然也赚不到多少钱。这样,每个部门都会有一个 COM+ 应用程序分区。
我们可以使用默认的类对象,但将每个默认命名空间放入我们的公司名称 MyBusiness Ltd 中是值得的。您需要为每个项目添加 EnterpriseServices
命名空间。为此,请转到菜单“项目”->“添加引用”或在解决方案资源管理器中右键单击项目的“引用”选项卡,然后选择“添加引用”。这将打开如下所示的“添加引用”对话框。选择 System.EnterpriseServices
组件。
图 3 - 添加/删除项目引用
此外,您还必须将 MyBusiness.Personnel
和 MyBusiness.Orders
项目和命名空间添加到 MyBusiness.Administration 项目中,因为 Admin 对象将调用部门维护对象。
开始编写代码 - Administration 对象
神奇般地,主要的管理业务对象出现了。这 all 非常牵强,但请跟着我。我将指出一些将一个类变成可以在 COM+ 中使用的类的特性。
using System;
using System.EnterpriseServices;
using MyBusiness.Personnel;
using MyBusiness.Orders;
[assembly: ApplicationName("MyBusiness.Administration")]
[assembly: ApplicationActivation(ActivationOption.Library)]
namespace MyBusiness.Administration
{
/// <summary>
/// Epmployee Administration Object
/// </summary>
[ Transaction(TransactionOption.RequiresNew) ]
[ ObjectPooling(true, 5, 10) ]
public class EmployeeMaintenance : ServicedComponent
{
public EmployeeMaintenance()
{
//
// TODO: Add constructor logic here
//
}
[ AutoComplete(true) ]
public void AddEmployee(string Name, string Address, int JobType,
bool bMakePayrollFail, bool bMakeOrdersFail)
{
// Create out tier 3 of 4 components that act as the data access layer.
PayrollMaintenance payroll_maintenance = new PayrollMaintenance();
OrdersMaintenance orders_maintenance = new OrdersMaintenance();
// Some business Logic...Names must always be stored in upcase!
Name = Name.ToUpper();
// Let the tier 3 of 4 access the seperate databases and store
// our complex example business information.
payroll_maintenance.AddEmployee(Name, Address, JobType, bMakePayrollFail);
orders_maintenance.SetupUser(Name, JobType, bMakeOrdersFail);
}
}
}
我们的 EmployeeMaintenance
类是从 ServicedComponent
类派生的,该类是对象能够愉快地在 COM+ 运行时服务下运行并利用其服务的核心。ServicedComponent
是从 ContextBoundObject
派生的,而 ContextBoundObject
又从 MarshalByRefObject
派生。
ServicedComponent
具有以下重写,您以后可能会发现它们很有用。
void Activate();
bool CanBePooled();
void Construct(string s);
void Deactivate();
接下来,我们的 EmployeeMaintenance
类有两个相关属性。
[ Transaction(TransactionOption.RequiresNew) ]
[ ObjectPooling(true, 5, 10) ]
此对象要求它存在于自己的新事务中,无论它是在另一个事务上下文中创建的。在此类使用的其他组件中,它们被标记为 Requires。它们需要一个事务,但乐于使用它们被创建的事务,否则它们将创建一个新事务。这就是两个或多个数据库如何共享同一事务。
对于客户端,此类只有一个方法值得关注,称为 AddEmployee
。
[ AutoComplete(true) ]
public void AddEmployee(string Name, string Address, int JobType,
bool bMakePayrollFail, bool bMakeOrdersFail)
该方法被标记为 AutoComplete
属性,它实现了一个有用的功能,即如果没有抛出异常,则将其事务部分标记为“确定”。这有助于减少所需的代码量。如果实现将 AutoComplete
设置为 false,或完全省略它,那么我们就需要手动管理事务。要手动控制事务,您需要使用 ContextUtil
类及其静态成员,我建议您在 MSDN 上查看它们。下面有一个简短的摘录,展示了如何手动使用 ContextUtil
类。
public void SampleFunction()
{
try
{
// Do something to a database
// ...
// Everything okay so far Commit the transaction
ContextUtil.SetComplete();
}
catch(Exception)
{
// Something went wrong Abort and Rollback the Transaction.
ContextUtil.SetAbort();
}
}
手动控制事务作为 [AutoComplete(true)]
的替代方案
ContextUtil
类的 SetComplete
和 SetAbort
方法作用于它的两个属性。它们是:
ContextUtil.MyTransactionVote
- 用于将事务标记为到目前为止是“确定”的。
ContextUtil.DeactivateOnReturn
– 表示无需再进行任何操作,相对于 MyTransactionVote
,进行提交或回滚。
如果您计划构建一个在提交事务之前调用对象中多个函数的对象,那么使用更细粒度的方法而不是将其设置为 AutoComplete 将非常重要。
回到我们的类,我想指出的是,函数末尾有两个参数,允许客户端应用程序指示其中一个组件抛出异常。我想这样做是为了轻松地演示,即使在使用的组件之一打开了数据库、修改了它并成功关闭了连接(就它而言),数据直到 COM+ 运行时发出信号后才被提交。它在等待的是拥有事务的根对象在整个上下文的一部分中告诉它提交事务。
Personnel Maintenance 类
我将只展示部分代码,而不是完整的列表。
using System;
using System.Data;
using System.Data.OleDb;
using System.EnterpriseServices;
[assembly: ApplicationName("MyBusiness.Personnel")]
[assembly: ApplicationActivation(ActivationOption.Library)]
namespace MyBusiness.Personnel
{
/// <summary>
/// Payroll Specific Mainenance Object
/// </summary>
[ Transaction(TransactionOption.Required) ]
[ ObjectPooling(true, 5, 10) ]
public class PayrollMaintenance : ServicedComponent
{
public PayrollMaintenance ()
{
//
// TODO: Add constructor logic here
//
}
public void AddEmployee(string Name, string Address, int JobType,
bool MakeFail)
{
string sConnection = "Provider=SQLOLEDB;Data Source=localhost;" +
"Initial Catalog=MyPersonnelDB;" +
"Trusted_Connection=Yes";
OleDbConnection cnn= new OleDbConnection(sConnection);
// Open the Database Connection
cnn.Open();
// .....
// Open the DataAdapter, DataSets and open a new Row
// See the code in the example
// .....
dr["sName"] = Name; ;
dr["sAddress"] = Address;
dr["nJobType"] = JobType;
dr["sTransactionActivityID"] = ContextUtil.ActivityId;
dr["sTransactionContextID"] = ContextUtil.ContextId;
ds.Tables["tblEmployees"].Rows.Add(dr);
da.Update(ds, "tblEmployees");
// Close the Database Connection
cnn.Close();
if(MakeFail)
{
// Oh no!!! Its all gone horibly wrong.
throw new Exception("User requested Exception in " +
"PayrollMaintenance.AddEmployee");
}
}
}
}
现在请注意,TransactionOption
设置为 Required
。我之前提到了这一点,说由启动事务的对象调用的组件应该使用该事务。这允许它这样做,而不是 RequiresNew
,后者将启动另一个事务,从而破坏事物。
[ Transaction(TransactionOption.Required) ]
在数据库中,我还想存储 ContextUtil
类的一些属性,该类代表当前事务。这些是 ActivityID
和 ContextID
。正如您将看到的,当我更新两个数据库中的单独对象时,它们都将看到相同的事务 ContextID
。
.NET 中的 COM+ 组件是强命名程序集
派生自 ServicedComponent
的组件需要强命名才能在 COM+ 下运行。这意味着它需要使用公钥/私钥对进行数字签名。那么,我该如何创建一个密钥,您可能会问?这个很简单——启动 VS.NET 命令提示符,然后查看强命名工具 sn.exe,如下所示。要构建自己的强命名,请将 –k 作为第一个参数,然后是您想要的输出签名文件的名称。
图 4 - 运行强命名工具创建新密钥
一旦有了文件,对于每个 Serviced Component 库,您都需要在其所属的库项目的每个程序集文件中引用它。需要的属性会自动填充到 AssemblyInfo.cs 文件的底部,但缺少强命名密钥文件。
所以:
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("")]
[assembly: AssemblyKeyName("")]
变为
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("..\\..\\..\\MyBusiness.sn")]
[assembly: AssemblyKeyName("")]
数据库
如果您从上面下载了示例解决方案,其中包含 SQL Server 数据库的创建脚本,位于“数据库创建脚本”文件夹下。示例需要两个数据库,每个数据库有一个表,如果您手动创建的话。
MyPersonnelDB
tblEmployees
字段名 类型 长度 其他 nEmployeeID int 4 主键 & 索引 sName nvarchar 50 sAddress nvarchar 200 nJobType int 4 sTransactionActivityID nvarchar 50 sTransactionContextID nvarchar 50 MyOrdersDB
tblOrderUser
字段名 类型 长度 其他 nEmployeeID int 4 主键 & 索引 sName nvarchar 50 nJobType int 4 sTransactionActivityID nvarchar 50 sTransactionContextID nvarchar 50
您会注意到我添加了两个额外的字段。这些字段由两个数据访问层对象填充,它们认为它们是 ContextUtil
类的 TransactionActivity
和 TransactionContexts
属性。一切顺利的话,两个数据库都应该看到相同的事务上下文。
运行应用程序
如果您运行应用程序,您将看到以下屏幕:
图 5 - 测试 COM+ 应用程序的简单客户端
请记住输入一些示例文本,然后添加员工。如果您的机器像我的一样老旧,它会卡顿几秒钟。这是因为组件是第一次初始化。由于这些是池化对象,第二次及以后会快得多。
然后您应该查看数据库内容,以确认新条目已添加到两个数据库中。如果您重新运行上述操作,但启用了其中一个强制异常,那么整个事务将回滚,即使数据库连接已从代码中打开然后关闭。
最后,如果您从“控制面板”->“管理工具”打开组件管理器,您将看到这 3 个组件库已创建,并且从“事务统计信息”中,您应该可以看到事务快速通过,或者那些被中止的。
图 6 - 用于管理 COM+ 应用程序的组件服务控制台
在图 6 的底部,您应该在“分布式事务协调器”下的“事务统计信息”中看到。从这个屏幕,您可以查看系统上运行的事务的状态和结果。请注意,在运行客户端应用程序后,并且每次添加员工时,“事务聚合”都会增加——这是您正在运行的事务。
进一步开发中值得关注的点
我没有在代码中实现的一个效果很好的功能是使用消息队列来记录失败事务的详细信息,以便管理员稍后诊断。这里需要注意的重要一点是不要使用事务性队列,因为它将在事务上下文中运行并被回滚,从而使之徒劳无功。我没有提到消息队列可以是事务性的吗?嗯,下次再说。
历史
2003 年 3 月 24 日 - 初版