使用 C#、OpenXML 和 Microsoft SQL Server 2000 创建多线程规则引擎 Web 服务






3.92/5 (5投票s)
使用 OpenXML 在几分钟内创建一个简单、可扩展的 XML 解析规则引擎。
引言
本文使用 C#、OpenXML 和 Microsoft SQL Server 2000 创建了一个多线程规则引擎 Web 服务。
背景
似乎如今,人们对规则引擎的关注度很高。我猜想,自从微软一直宣传 BizTalk 是解决业务规则引擎问题的方案以来,很多人都注意到了。但 BizTalk persistent 的问题是它非常昂贵。它是拥有雄厚资源的公司的解决方案。它不是那些只需要简单、功能性规则引擎的小公司的解决方案。现在,不要误会我的意思;我将在这里介绍的解决方案并不是规则引擎的“万能钥匙”。有无数种方法可以执行 XML 规则验证,而我在这里只展示其中一种。如果您想要一个更简单的规则引擎,可以查看 Moustafa 的可扩展轻量级 XML 规则引擎组件。如果您想要更强大但又更复杂一些的东西,可以查看 Nxbre 的 .NET 业务规则引擎。虽然配置通常更复杂,但它确实遵循 RulesML 0.86 datalog,而我的示例并不旨在做到这一点。我将在这里详细介绍的示例(在此冗长的前言之后)旨在读取任何 XML 文档中的字段,并将其与静态规则集进行评估。我将添加的最后一点内容是:这是一个原型。它并非旨在解决您的流程问题。它旨在帮助建议解决这些问题的可能途径。正如我之前所说,有很多方法可以实现这一点,如果您想要一个开箱即用的解决方案,我建议您查看 Microsoft 的 BizTalk Server。此解决方案是众多解决方案之一,有望呈现一种易于实现的解决方案。因此,会有许多元素被省略,但可以通过少量额外代码轻松实现。
那么,为什么选择基于 SQL 的方案?
根据我编写业务应用程序的经验,每个人都有遗留数据问题。许多公司拥有不灵活的 XML 文档,无法满足 RulesML 0.86 的要求。它们通常有自己的 XML 格式,由遗留业务应用程序使用。当然,您可以创建一个 XSLT 样式表,将所有 XML 转换为商业规则引擎所需的格式,然后继续进行。但我发现,XSLT 创建起来可能非常繁琐且耗时,尤其是在 XML 文档可能包含数千个字段,或者 XML 频繁更改的情况下(请注意 XSLT 拥护者的评论 :))。而且,我很懒,所以 XSLT 对我来说很难有效地使用。我宁愿利用现有的业务 XML 文档的架构,因为毫无疑问,会有其他应用程序使用该 XML。此外,我对 XSLT 处理包含超过 40 个节点的文档的性能并不完全满意。
问题的另一个关键点是简单性:可伸缩性。这个 SQL 规则引擎可以轻松地进行即时修改。我发现大多数业务分析师都对结构化查询语言有半吊子了解,或者,如果不是,也可以轻松学会。这个规则引擎可以通过编辑 SQL Server 中的存储过程来扩展,而无需更改引擎本身的代码并重新编译/重新部署。通过这种方式,可以即时添加、删除和修改规则。部署极其简单(只需脚本化数据库……您无需完全修改 Web 服务),而且考虑到所有因素,OpenXML 相当快,即使处理包含超过 200 个节点的大型文档也是如此。(请参阅本文档末尾的性能结果。)
我需要什么?
由于我们将 Web 服务的这部分设计为使用 C#,因此您需要一个 C# 编译器。此示例使用 Visual Studio 2003。您还需要访问运行 MS SQL 2000 的服务器,并在您将使用的数据库上拥有 DBO 权限,当然,还需要一个运行至少 IIS 5.1 的 Web 服务器。该项目在 SQL 方面的比重比 C# 大,因此需要对 SQL 有良好的工作理解。
开始项目
设置 IDE
打开 Visual Studio .NET 并创建一个新项目。选择 Visual C# Projects,然后选择 ASP.NET Web 服务。可以使用您喜欢的任何名称(此示例称为 XRules)。新项目加载后,右键单击 *Service1.asmx*,然后选择“删除”。接着,右键单击您的解决方案,选择“添加->新项”,从列表中选择 Web 服务,然后为名称键入 *XRules.asmx*。单击“确定”。然后,再次右键单击您的解决方案,选择“添加->新类”。键入“*engine.cs*”作为类名,然后单击“确定”。现在 IDE 已按照我们的意愿进行设置。
工作原理
我们的 Web 服务只是一个执行接口,用于触发我们 SQL Server 上的存储过程。存储过程和 SQL Server 完成了几乎所有的实际工作。我们将在 Web 服务中做的所有事情是提供一个传递 XML 文档的门户以及执行线程来验证 XML 文档的方法。
设置 SQL Server
现在我们已经配置了 IDE,让我们继续设置 SQL Server。打开 Enterprise Manager,并创建一个新数据库。您也可以使用现有数据库,前提是您拥有对它的 DBO 权限。本示例将引用 XRules 数据库(很有道理,对吧?)。创建数据库后,打开 SQL Query Analyzer,并运行以下脚本
首先,我们创建将存储我们所有规则的表。这些仅仅是指向存储我们规则的存储过程的指针,但我们需要此表来多线程执行引擎。
if exists (select * from dbo.sysobjects where
id = object_id(N'[dbo].[XRules]') and
OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [dbo].[XRules]
GO
CREATE TABLE [dbo].[XRules] (
[RuleGUID] AS (newid()) ,
[ExecuteSP] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
) ON [PRIMARY]
GO
现在,我们将创建一个 [Errors] 表来存储规则执行期间发生的所有错误。
if exists (select * from dbo.sysobjects where
id = object_id(N'[dbo].[XErrors]') and
OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [dbo].[XErrors]
GO
CREATE TABLE [dbo].[XErrors] (
[ErrorId] [bigint] IDENTITY (1, 1) NOT NULL ,
[ErrorMessage] [varchar] (255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
[ErrorTimestamp] [datetime] NULL ,
[RuleGUID] [uniqueidentifier] NULL
) ON [PRIMARY]
GO
最后,我们将创建这个可选表,它会将执行性能记录到我们的数据库中。这对于确定各种规则集执行的速度以及确保引擎确实在多个线程上执行了您的规则非常有用。
if exists (select * from dbo.sysobjects where
id = object_id(N'[dbo].[Performance]') and
OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [dbo].[Performance]
GO
CREATE TABLE [dbo].[XPerformance] (
[PerformanceId] [bigint] IDENTITY (1, 1) NOT NULL ,
[RuleGUID] [uniqueidentifier] NULL ,
[ProcessStart] [datetime] NULL ,
[ProcessEnd] [datetime] NULL
) ON [PRIMARY]
GO
编写引擎
好的,现在我们已经设置好了通用框架,是时候编写执行引擎了。我将编写一个更复杂的(理论上,只有……此示例对中等到初级程序员来说相当不复杂)示例,它将利用 System.Threading
命名空间。当然,您可以在单个线程上执行相同的操作,但多线程应用程序应该会带来不错的性能提升。
Engine 类
using System;
using System.Data;
using System.Threading;
using System.Data.SqlClient;
using System.Xml;
/// <SUMMARY>
/// Contains threaded methods for validating SQL server based rules.
/// </SUMMARY>
public class engine
{
//The xmldocument we want to validate
private XmlDocument xDoc;
public engine(XmlDocument xml)
{
xDoc=xml;
}
public void validateRules()
{
//Instantiate our SQL connection
string connString =
System.Configuration.ConfigurationSettings.AppSettings["DBConn"];
SqlConnection cn = new SqlConnection(connString);
string strSql = "SELECT * FROM XRules";
SqlDataAdapter adapter = new SqlDataAdapter(strSql, cn);
DataTable dt = new DataTable("Rules");
try
{
cn.Open();
adapter.Fill(dt);
}
catch (Exception exc)
{
//To-do: Log this exception to the database.
throw exc;
}
finally
{
cn.Close();
}
foreach (DataRow r in dt.Rows)
{
validator v = new validator();
v.executeSP = r["ExecuteSP"].ToString();
v.ruleGuid = new Guid(r["RuleGUID"].ToString());
v.connString = connString;
v.xDoc = xDoc;
Thread vThread = new Thread(new ThreadStart(v.validateRule));
vThread.Start();
}
}
}
Validator 类
public class validator
{
//The string of the Stored Procedure we want to execute
public string executeSP;
//The guid of the Stored Procedure we want to execute
public Guid ruleGuid;
//The xml to validate
public XmlDocument xDoc;
//The connection string to the server
public string connString;
//Validate each rule in the table on a separate thread.
public void validateRule()
{
SqlConnection cn = new SqlConnection(connString);
SqlCommand cmd = new SqlCommand(executeSP, cn);
//Pass the GUID of the rule as a parameter (for error logging)
cmd.Parameters.Add(new SqlParameter("@RuleGUID", ruleGuid));
//Pass the xml document's inner XML to the SP to use in validation
cmd.Parameters.Add(new SqlParameter("@xml", xDoc.InnerXml));
cmd.CommandType = CommandType.StoredProcedure;
try
{
cn.Open();
cmd.ExecuteNonQuery();
}
catch (Exception exc)
{
//To-do: Log this exception to the database using the provided ruleGuid.
throw exc;
}
finally
{
cn.Close();
}
}
}
发生了什么?
我所做的只是创建了一个基本的框架,用于在单独的线程上执行存储过程列表。正如我之前所说,大部分规则验证是在 SQL Server 端完成的。我们需要传递给存储过程的只是 XML 文档(InnerXml
)以及我们正在执行的规则的 GUID。我们可以在规则存储过程本身中执行 GUID 查找,但这可以通过在运行存储过程时实例化它来节省一个 SQL 语句。因此,就像委托一样,我们将为每个规则集传递一对参数;GUID 和 XML。通过这种方式,我们可以确保设计规则集的方法一致。
Web服务
现在我们已经编写了 C# 引擎的“精华”,是时候通过 Web 服务公开这些方法了。请打开您的 *XRules.asmx* 页面,并切换到代码视图。您需要将 System.Xml
添加为引用。我在下面提供了两个示例(可以使用其中一个或两个)。
/// <SUMMARY>
/// Evaluates an Xml file with a given file path.
/// </SUMMARY>
/// <PARAM name="xmlFilePath"></PARAM>
[WebMethod]
public void Evaluate(string xmlFilePath)
{
try
{
XmlDocument xDoc = new XmlDocument();
xDoc.Load(xmlFilePath);
engine e = new engine(xDoc);
e.validateRules();
}
catch (Exception exc)
{
//To-do: Add exception logging to the database here.
throw exc;
}
}
/// <SUMMARY>
/// Evaulates a local copy of the books.xml file.
/// </SUMMARY>
[WebMethod]
public void EvaluateLocal()
{
try
{
XmlDocument xDoc = new XmlDocument();
xDoc.Load(Server.MapPath("books.xml"));
engine e = new engine(xDoc);
e.validateRules();
}
catch (Exception exc)
{
//To-do: Add exception logging to the database here.
throw exc;
}
}
我们正在从路径加载 XML 文档,或使用服务器映射直接加载文件。当然,您可以更改方法以接受方法中的 XML 文档对象,但目前我们保持简单。一个可能的未来实现(如果您真的想对多线程感到兴奋)是,如果您需要加载一组 XML 文档,则对这些方法进行多线程处理。例如,如果您有一个目录包含几百个 XML 文档需要通过引擎进行处理,您可以遍历该目录并生成一个线程来处理每个文档。当然,这可能会因为打开和关闭的连接数量过多而让您的 DBA 感到头疼,但有时保持他们的警惕也很有趣。
最后,请确保为用于连接到 SQL Server 本地实例的连接字符串在您的 *Web.config* 文件中添加一个关键设置。(请参阅下载中的示例。)
编写规则
XML 文档
好的,我们快完成了!我们已经创建了 Web 服务和执行我们规则集的引擎。现在,我们需要创建几个测试规则集来执行,当然,还需要一个 XML 文档来用于我们的测试。我们将使用行业标准的 *books.xml* 作为我们想要评估的 XML 文档。只需下载 XML,并将其保存到您的项目中,命名为“*books.xml*”。
第一个规则集
我们将要组装的第一个规则集示例是一个简单的规则集,它确保每本书的价格字段在 0 美元到 10 美元之间。您可以以几种方式进行此检查,但为了说明目的,我们将分别检查每个项目,从而有效地进行两个规则评估。
在 Enterprise Manager 中,选择您之前创建的数据库(XRules),然后选择“存储过程”节点。在右侧窗口中,选择“新建存储过程”。默认的存储过程编辑器将打开。将下面的代码复制并粘贴到编辑器中,然后单击“确定”。
CREATE PROCEDURE [dbo].[XRE_Prices]
@xml NTEXT,
@RuleGUID uniqueidentifier
AS
--Performance Logging. You can comment this out
--if you don't want to log performance.
INSERT INTO XPerformance(RuleGUID, ProcessStart)
VALUES (@RuleGUID, getdate())
--Handle to the in-memory XML document
DECLARE @idoc int
--Create an in-memory table to store our book data
DECLARE @Books TABLE (Price money)
--Create an internal representation of the XML document
EXEC sp_xml_preparedocument @idoc OUTPUT, @xml
--Execute a SELECT statement that uses the OPENXML rowset provider
--We will select all books from the document,
--and insert them into our temp table for querying
INSERT INTO @Books (Price) (SELECT * FROM OPENXML
(@idoc, 'bookstore/book',2) WITH (price money))
-- Clear the XML document from memory
EXEC sp_xml_removedocument @idoc
/****** RULE SECTION ******/
DECLARE @count int
/* --- RULE 1: Check for prices <= 0 --- */
SET @count = (SELECT COUNT(Price) FROM @Books WHERE Price <= 0)
IF (@count > 0)
BEGIN
INSERT INTO XErrors (ErrorMessage, ErrorTimestamp, RuleGUID)
VALUES ('Located ' + CONVERT(varchar(10), @count) +
' books with price <= 0',getdate(),@RuleGUID)
END
/* --- RULE 2: Check for prices >= 10 --- */
SET @count = (SELECT COUNT(Price) FROM @Books WHERE Price >= 10)
IF (@count > 0)
BEGIN
INSERT INTO XErrors (ErrorMessage, ErrorTimestamp, RuleGUID)
VALUES ('Located ' + CONVERT(varchar(10), @count) +
' books with price >= 10',getdate(),@RuleGUID)
END
/****** END RULE SECTION ******/
--Performance Logging. You can comment this out
--if you don't want to log performance.
UPDATE XPerformance SET ProcessEnd = getdate()
WHERE RuleGUID = @RuleGUID
GO
上述规则集应该可以无问题地保存。如果您遇到语法错误,很可能是因为您有一个无效的表列名或无效的表名。只需检查您的语法,然后再次保存。我们再创建一个规则集,然后我将详细解释幕后发生的事情。由于 Melville 的“The Confidence Man”价格超过 10.00 美元,此规则应该会产生一个错误。
值得注意的几点
您可能已经注意到了紧跟在 bookstore/book
后面的“2”参数。bookstore/book
当然是我们正在查找的元素的 XPath。OpenXML 允许我们指定 XML 映射的元素中心、属性中心或两者兼有。属性中心通过使用“1”参数指示(如下面的示例所示);“2”指定元素中心,“8”指定两者都搜索,但会首先利用属性中心。使用“0”将默认为元素中心。您可以在 此 MDSN 站点上找到大量示例。
第二个规则集
在此规则集中,我们将评估每个流派属性。注意 XPath 的变化。我们现在要选择一个属性,而不是节点文本。关于 XPath 查询和 OpenXML 的更多信息,可以在 Perfect Xml 上找到很棒的文章。我们将在此查找中使用属性中心标志。
CREATE PROCEDURE [dbo].[XRE_Genres]
@xml NTEXT,
@RuleGUID uniqueidentifier
AS
--Performance Logging. You can comment this
--out if you don't want to log performance.
INSERT INTO XPerformance(RuleGUID, ProcessStart)
VALUES (@RuleGUID, getdate())
--Handle to the in-memory XML document
DECLARE @idoc int
--Create an in-memory table to store our genre data
DECLARE @Genres TABLE (Genre varchar(50))
--Create an internal representation of the XML document
EXEC sp_xml_preparedocument @idoc OUTPUT, @xml
--Execute a SELECT statement that uses the OPENXML rowset provider
--We will select all books from the document,
--and insert them into our temp table for querying
DECLARE @genre varchar(50)
SET @genre = (SELECT * FROM OPENXML (@idoc, 'bookstore/book',1)
WITH (genre varchar(50)))
INSERT INTO @Genres (Genre) VALUES (@genre)
-- Clear the XML document from memory
EXEC sp_xml_removedocument @idoc
/****** RULE SECTION ******/
DECLARE @count int
/* --- RULE 1: Ensure genre is not = 'Philosophy' --- */
SET @count = (SELECT COUNT(Genre) FROM @Genres WHERE Genre = 'Philosophy')
IF (@count > 0)
BEGIN
INSERT INTO Error (ErrorMessage, ErrorTimestamp, RuleGUID)
VALUES ('Located ' + @count +
' books with genre of Philosophy.',getdate(),@RuleGUID)
END
/****** END RULE SECTION ******/
--Performance Logging. You can comment this out
--if you don't want to log performance.
UPDATE XPerformance SET ProcessEnd = getdate() WHERE RuleGUID = @RuleGUID
GO
现在,在上述规则集中,如果我们 XML 文档中有任何书籍的流派是哲学,我们将发出错误消息。由于 Plato 的“The Gorgias”是一本关于哲学的书,这将产生一个错误。
整合
好的,我们的规则集已创建!现在,我们只需要将规则集添加到我们的 *XRules* 表中,然后我们就可以开始工作了!只需在 Enterprise Manager 中打开您的 *XRules* 表,并将 XRE_Prices
和 XRE_Genres
添加到 ExecuteSP
列中。我们已经让 SQL Server 为我们生成了唯一标识符,因此完成后,只需关闭并保存表。您可以通过重新打开表或运行一个简单的 SELECT * FROM XRules
来检查表以确保 GUID 已正确生成。
幕后
正如我们在编写引擎时所看到的,所有发生的事情是 Web 服务从 *XRules* 表中加载每个规则集。然后它生成一个线程,并在自己的线程上执行每个存储过程。该过程使用 OpenXML 消耗 XML,并从 XmlDocument
中选择所需的字段。然后我们将这些值加载到临时表中,使用我们的规则查询该表,然后在规则失败时发出错误消息。完成后,我们可以拥有一个 Web 服务方法,该方法可以(可能)向用户返回错误列表,或者选择错误计数,如果错误计数大于 0,则向 Web 用户返回失败。这部分留给您自行决定。
常见问题解答
问:为什么使用内存表而不是游标或临时表?
答:主要是为了性能。当使用临时表时,SQL Server 会在事务完成之前锁定 *tempdb* 数据库。游标速度很快,但需要大量磁盘 I/O,并且对于不熟悉 VSAM 或 ISAM 数据库环境的人来说,游标很难操作。创建内存表变量仅使用 SQL Server 上的 RAM 内存,并且不需要数据库锁定(摘自 Peter Bromberg 的文章)。
问:为什么只有三个规则要有两个单独的存储过程?
答:主要是为了说明。据推测,业务规则集的架构师将有数百甚至数千条规则需要评估。为了易于使用和提高效率,将每条规则分开以评估 XML 的不同部分,在组织上是最好的。如果您将所有规则都放在一个存储过程中,那么就没有理由让它在多个线程上运行。我在这里提供了两个示例规则集作为示例。您可以随时添加自己的规则。
问:这个是否经过了性能测试?
答:仅在名义负载下进行。在测试环境中,此规则引擎评估了 100 个长度相等(200 个评估节点)的 XML 文档,针对 10 个规则集,每个规则集包含 10 条规则。结果是,从 Web 服务中生成了 10 个线程,每个线程的平均执行时间在 0.2 秒到 0.5 秒之间,每个文档的平均完成时间不到 1 秒。此测试是在 MSSQL 2000、IIS 5.1 和双 Intel Xeon 处理器 @ 3.19 GHz(每个拥有 1024 KB RAM)下运行的。我假设生产 SQL Server 能够实现更高的性能。
问:我如何知道抛出了什么错误(如果有的话)?
答:只需运行“SELECT Count(ErrorId) FROM XErrors
”来找出其中有多少错误。由于我们在抛出错误时记录了 RuleGUID
,因此我们可以轻松知道错误发生在哪里。您也可以在 Web 服务中处理异常,方法是将错误记录到数据库(如示例代码中所示)。
关注点
正如我之前所说,此解决方案可能不具备商业可行性。此解决方案的某些部分仍处于实验阶段,这是第一个可工作的原型。在那些我不想修改现有 XML 文档的情况下,以及在我想拥有一个可以通过 Web 服务公开访问的规则引擎的情况下,它的效果非常好。如果您中的任何一个人使用此方法,我将非常乐意听听它的效果,以及您可能对代码或 SQL 所做的任何更改。提供的源代码是一个原型;我只概述了启动概念。您需要的一切都已包含在此文章中,因此它至少应提供一个良好的路线图(我希望!)。它的优点在于其简单性。您可以在几分钟内拥有一个可工作的规则引擎,您或各种业务分析师可以轻松地修改(或添加)存储过程来评估正确的 XML 节点。编码愉快!