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

使用 bsn-goldparser 和 CodeDom 实现实体映射语言

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2012 年 4 月 5 日

CPOL

17分钟阅读

viewsIcon

29873

downloadIcon

436

使用 GOLD Parser 定义一种映射两种业务实体之间关系的语言,使用 bsn-goldparser 引擎创建解析器,并使用 CodeDom 生成程序集。

Grammar testing form

引言

此程序展示了如何使用 GOLD Parser 定义一种简单语言并生成由解析器引擎使用的语法表。使用的解析器引擎来自 bsn-goldparser,它支持使用 C# 创建解析器。解析器实现使用 CodeDom 生成编译单元,最终将其传递给 C# 代码编译器以生成 C# 源代码和程序集。

此程序为定义简单的领域语言指明了方向,允许用户使用语法编辑业务规则,翻译它们,并动态生成供主程序使用的程序集。试想一下,这些业务规则只是可以存储在数据库表中、用户随时可以检索和编辑的字符串值,并且最终可以在不修改任何代码的情况下集成到应用程序中。这无疑解决了软件开发人员今天面临的问题,即不同的客户即使来自同一商业领域,也有其独特的业务规则集,更糟糕的是,这些规则总是在变化。此程序演示了实体映射规则的实现,这是在面对实体创建和相关实体(例如,从相关销售订单开具发票)的挑战时典型的用法。

对于刚接触 bsn-goldparser 的人,我强烈建议您阅读这篇名为 The Whole Shebang: Building Your Own General Purpose Language Interpreter 的文章,它对这个解析器引擎和语言解析主题进行了出色的介绍。

背景

在讨论我创建的程序之前,我先解释一下我要解决的问题。在许多业务应用程序中,我们经常需要创建一个实体,其属性是从现有实体的属性映射或转换而来的。例如,请看下图。我们需要将发票对象(InvoiceDate)的发票日期设置为比订单交易日期(TxDate)晚 30 天,将订单号(OrderNo)映射到相关合同号(ContractNo),并将运费(Freight)设置为根据订单的 CBM(TotalCBM)属性值应用公式的结果。这样的业务规则可以硬编码在程序中,但长远来看,当这些规则发生变化时,会存在代码修改的风险。当然,经验丰富的开发人员会通过采用设计模式来解决这些问题,将实现细节分离到库中,从而在需要修改时使他们的工作更轻松。但是,如果我们想创建一个商业软件包来满足成百上千个客户以及所有可能的业务规则组合,那么创建许多不同的程序集实现仍然是一场噩梦。为了满足如此 vast 的需求变化,我们可以创建一个由简单领域语言支持的规则编辑器,并允许用户编辑业务规则以满足他们的业务需求。

Order to invoice mapping

关键在于我们使用哪种语言来存储源代码,是 C# 还是 VB?当然,您可以这样做,特别是 .NET 框架足够灵活,可以从存储中读取 C#/VB,动态编译并加载到内存空间。这当然是解决问题的一种方法,但对于负责维护业务规则的用户来说,这并不太有用,因为他们不太可能理解这些计算机语言。可能的解决方案是定义简单的语言构造,用户容易理解,并且足够先进,可以解决领域问题。

这个业务规则创建使用了类似于 C# 语句和表达式语法的领域语言。我使用 GOLD Parser 来编辑我的领域语言,并使用 bsn-goldparser CodeDom 及相关工具动态生成程序集。尽管业务规则中使用的领域语言采用了 C# 语句和表达式构造的简化版本,但请不要被它迷惑,您可以尽情发挥想象力,构建另一种非常冗长的类英语领域语言供用户输入业务规则。我定义语法像 C# 语句和表达式的原因只是为了在最短的时间内生成示例,并且 C# 是我最熟悉的语言。

使用 GOLD Parser 和相关引擎创建特定的解析器实现并不是新鲜事,但大多数将它实现为解释器的示例对于集成到主应用程序来说并不是很有用。在审查了不同的选项,如 IL Emit、.NET 4.0 ExpressionCodeDom 来生成动态代码后,我发现 CodeDom 尤其容易被采用,特别是如果您有 .NET 背景。它带来的其他好处是它提供了一个语言中立的程序图(代码编译单元),并且该图可以被序列化以便以后加载以提高性能。

使用代码

运行示例

您只需要 Visual Studio 2010 即可打开并运行下载源代码中的示例程序,无需依赖其他库。有一个测试窗体,预填充了来自 Sampler 类的规则,如下所示。此示例使用两个模型类:SalesOrderInvoice,分别由 GetOrder()GetInvoice() 方法返回。示例属性映射规则由 GetMappingStatements() 方法返回,它们应该很容易理解,没有太大困难。实际上,规则语句展示了我们可以将属性映射到目标实体(Invoice 对象)的多种方式。除了将值从源属性的派生赋值给目标外,我们还可以调用全局类方法来为目标属性赋值。例如,在此示例中,发票号(InvoiceNo)被赋值为全局服务类方法 GetNextInvoiceNo() 的返回值。

// -------------------------------------------------------------------
// Listing 1.
// -------------------------------------------------------------------
public class Sampler
{
   static public SalesOrder GetOrder()
    {
        return new SalesOrder { TxDate = DateTime.Today, 
               ContractNo = "A1123", TotalCBM = 5.5m };
    }

   static public Invoice GetInvoice()
    {
        return new Invoice();
    }

    static public string GetMappnigStatements()
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("InvoiceDate = source.TxDate.AddDays(30) ;\r\n");
        sb.Append("InvoiceNo = BusinessService.Instance.GetNextInvoiceNo() ;\r\n");
        sb.Append("OrderNo = source.ContractNo ;\r\n");
        sb.Append("Freight = (source.TotalCBM - 1.5) * 2.2 ;\r\n");
        sb.Append("CreateDate = DateTime.Today ;\r\n");
        return sb.ToString();
    }
}

您可以编辑业务规则并单击 Parse 按钮进行解析测试。每条业务规则都以目标实体属性名开头,后跟一个 "=" 字符,以及由源属性(source 是代表源实体的关键字)、全局类方法/属性或常量组成的表达式。每条规则必须以分号 ";" 结尾。查看本文顶部的第一个图,您会发现每条语句看起来都像标准的 C# 赋值语句,您只需在语句的右侧指定目标属性,而无需使用对象限定符。如前所述,领域语言的设计掌握在您手中,我只使用了简化的 C# 语法来节省创建此示例程序的时间。当然,如果您添加了任何新的语言标记或规则,您需要相应地更改 bsn-goldparser 的语义操作类实现,但这并不难,您可以参考示例程序是如何完成的。

单击 Parse 按钮后,您可以在主测试窗体的下方窗格中找到生成的 C# 源代码,如下所示。这是一个只有一个方法的类,该方法的主体中的语句反映了输入的业务规则。

// -------------------------------------------------------------------
// Listing 2.
// -------------------------------------------------------------------
namespace EntityMapper.CodeGen {
    using System;
    using EntityMapper.Service;
    
    public class MapperUtility {        
        public void MapEntity(EntityMapper.Model.SalesOrder source, EntityMapper.Model.Invoice target) {
            target.InvoiceDate = source.TxDate.AddDays(30);
            target.InvoiceNo = BusinessService.Instance.GetNextInvoiceNo();
            target.OrderNo = source.ContractNo;
            target.Freight = ((source.TotalCBM - 1.5m) * 2.2m);
            target.CreateDate = DateTime.Today;
        }
    }
}

单击 Execute 按钮后,您可以通过运行编译后的业务规则来获取测试结果,针对两个示例实体,如下所示,您可以看到发票实体的属性已根据输入的规则进行了仔细更改。

------ Sales Order ------
TxDate = 4/4/2012 12:00:00 AM
ContractNo = A1123
TotalCBM = 5.5

------ Invoice before calling method ------
InvoiceDate = 1/1/0001 12:00:00 AM
InvoiceNo = 
OrderNo = 
Freight = 0
CreateDate = 1/1/0001 12:00:00 AM

------ Invoice after calling method ------
InvoiceDate = 5/4/2012 12:00:00 AM
InvoiceNo = I702692
OrderNo = A1123
Freight = 8.80
CreateDate = 4/4/2012 12:00:00 AM

其他用途

SalesOrderInvoice 类型的用法仅用于演示目的。实际上,您可以利用这里的代码将它们集成到您的应用程序中,用于任何类型的实体映射。有趣的是,相同类型的两个实体也可以映射,以根据特定规则提供克隆功能。甚至单个实体也可以映射回自身以支持转换,您可以想象这是一种定义并从数据库存储中检索的对象创建策略。以下示例展示了通过 EntityMapperGenerator 类使用映射功能的场景(附图),该类同时提供了解析和编译方法。

// -------------------------------------------------------------------
// Listing 3.
// -------------------------------------------------------------------

var mapperGenerator = new EntityMapperGenerator(); 

// ----------- Scenario 1 (mapping) ----------- 
// 1.1 Parse business rules. textBoxInput.Text contains business rule
// statements entered. Last parameter is array containing imported namespaces.
mapperGenerator.Parse(typeof(SalesOrder), typeof(Invoice), 
  textBoxInput.Text, new string[] { "System", "EntityMapper.Service" }); 

// 1.2 Compile and get the delegate which represents the mapping method.
// This example uses Action<SalesOrder, Invoice>
var mapperMethod1 = (Action<SalesOrder, Invoice>)_mapperGenerator.GetDelegate(
    new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });

// 1.3 Invoke method. salesOrderObj and invoiceObj are entity instances undergoing property mapping.
mapperMethod1(salesOrderObj, invoiceObj);

// ----------- Scenario 2 (cloning) ----------- 
// 2.1 cloningRules should contain same entity type cloning business rules.
mapperGenerator.Parse(typeof(Invoice), typeof(Invoice), cloningRules, 
                new string[] { "System", "EntityMapper.Service" }); 

// 2.2 Note that this example uses Action<Invoice, Invoice>
var mapperMethod2 = (Action<Invoice, Invoice>)_mapperGenerator.GetDelegate(
    new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });

// 2.3 invoiceObj1 maps its properties to invoiceObj2 and invoiceObj2
// is the target entity going to have property values changed.
mapperMethod2(invoiceObj1, invoiceObj2);

// ----------- Scenario 3 (entity creation) ----------- 
// 3.1 creationRules should contain entity creation business rules.
mapperGenerator.Parse(typeof(Invoice), typeof(Invoice), creationRules, 
                      new string[] { "System", "EntityMapper.Service" }); 

// 3.2 Note that this example uses Action<Invoice, Invoice>
var mapperMethod2 = (Action<Invoice, Invoice>)_mapperGenerator.GetDelegate(
    new string[] { "System.dll", Path.GetFileName(this.GetType().Assembly.CodeBase) });

// 3.3 newInvoiceObj's properties shall undergo transformation according to
// creation business rules, e.g. InvoiceDate = DateTime.Today; CustomerId = "Unknown";
var newInvoiceObj = new Invoice();
mapperMethod2(newInvoiceObj, newInvoiceObj);

Mapping scenarios

关注点

定义语言语法

在使用巴科斯-诺尔范式 (BNF) 定义语言时,我尝试找出满足当前需求的最小语法。因此,只形成了一个用于目标属性的赋值语句,它在语言规则 <Statement> ::= Identifier '=' <Expression> ';' 中显示,其中 Identifier 代表目标实体属性,该属性将从赋值语句右侧表达式的求值结果中获得值。

要遵循我所做的,请使用 GOLD Parser 定义语法并输入尽可能多的测试用例。如果它们都通过了,您可以使用该工具生成编译的语法表(.cgt)。请注意,从版本 5 开始,GOLD Parser 默认生成增强语法表(.egt),但 bsn-goldparser 只支持旧的编译语法表(.cgt)。因此,您需要选择 Project 菜单下的 Save the Tables 菜单项来生成所需表类型到文件。

"Start Symbol" = <Program>

! -------------------------------------------------
! Character Sets
! -------------------------------------------------

{ID Head}      = {Letter} + [_]
{ID Tail}      = {Alphanumeric} + [_]
{String Chars} = {Printable} + {HT} - ["\]

! -------------------------------------------------
! Terminals
! -------------------------------------------------

Identifier    = {ID Head}{ID Tail}*
StringLiteral = '"' ( {String Chars} | '\' {Printable} )* '"'
DecimalValue =  {Number}+ | {Number}+ '.' {Number}+
Source = 'source'
Target = 'target'

! -------------------------------------------------
! Rules
! -------------------------------------------------

! The grammar starts below
<Program> ::= <Statements>

<Expression>  ::= <Expression> '>'  <Add Exp> 
               |  <Expression> '<'  <Add Exp> 
               |  <Expression> '<=' <Add Exp> 
               |  <Expression> '>=' <Add Exp>
               |  <Expression> '==' <Add Exp>    !Equal
               |  <Expression> '<>' <Add Exp>    !Not equal
               |  <Add Exp> 

<Add Exp>     ::= <Add Exp> '+' <Mult Exp>
               |  <Add Exp> '-' <Mult Exp>
               |  <Mult Exp> 

<Mult Exp>    ::= <Mult Exp> '*' <Negate Exp> 
               |  <Mult Exp> '/' <Negate Exp> 
               |  <Negate Exp> 

<Negate Exp>  ::= '-' <Value> 
               |  <Value> 

!Add more values to the rule below - as needed

<Value>       ::= StringLiteral
               |  DecimalValue
               |  '(' <Expression> ')'
               |  <Member Access>
               |  <Method Access>

<Member Access> ::= <SourceTarget> '.' Identifier
               |  Identifier '.' Identifier
               | <Value> '.' Identifier

<Args>       ::= <Expression> ',' <Args>
               | <Expression> 
               |
               
<Method Access> ::= <SourceTarget> '.' Identifier '(' <Args> ')'
               | Identifier '.' Identifier '(' <Args> ')'       
               | <Value> '.' Identifier '(' <Args> ')'     
                                          

<SourceTarget> ::= Source
               |  Target

<Statement> ::= Identifier '=' <Expression> ';'

<Statements> ::= <Statement> <Statements>
               | <Statement>

准备 CodeDom 代码编译单元

要使用 CodeDom 生成代码和编译程序集,我们需要先定义和创建 CodeCompileUnit 对象。我已经将 CodeCompileUnit 对象的创建封装在 ClassTypeWrapper 类中,如下所示。该类的构造函数使用传入的命名空间类名导入命名空间(C# 中的 `using` 语句)来定义稍后要创建的主类。请确保您传入业务规则中要使用的命名空间,否则引用任何类型时都需要使用限定名称。

请注意,我已经将 CodeMemberMethod 的创建分离到另一个稍后讨论的类中,并通过调用同一类中的 AddMainMethod() 方法传递创建的 CodeMemberMethod 引用。这样做具有灵活性,可以创建 CodeMemberMethod,因为您应该知道稍后 CodeMemberMethod 的签名与业务规则支持的使用密切相关。例如,如果您要支持业务规则的另一种用法,例如保险业务中的保费计算,那么方法签名肯定不同。

为了获得映射器方法生成,我们有另一个类 MapCodeMemberMethod 来生成 CodeMemberMethod 引用,该引用将被添加到 ClassTypeWrapper 类中包装的编译单元的 MainClass 中(列表 4)。

// -------------------------------------------------------------------
// Listing 4.
// -------------------------------------------------------------------
public class ClassTypeWrapper
{
    public CodeCompileUnit CompileUnit { get; private set; }
    public CodeTypeDeclaration MainClass { get; private set; }
    public CodeMemberMethod MainMethod { get; private set; }

    public ClassTypeWrapper(string unitNamespace, string className, string[] importNamespaces)
    {
        CompileUnit = new CodeCompileUnit();

        // Default namespace
        CodeNamespace codeNS = new CodeNamespace(unitNamespace);

        // Import namespaces
        foreach (string ins in importNamespaces)
        {
            codeNS.Imports.Add(new CodeNamespaceImport(ins));
        }

        MainClass = new CodeTypeDeclaration(className);
        MainClass.IsClass = true;
        MainClass.TypeAttributes = System.Reflection.TypeAttributes.Public;
        codeNS.Types.Add(MainClass);
        
        CompileUnit.Namespaces.Add(codeNS);
    }

    public int AddMainMethod(CodeMemberMethod method)
    {
        this.MainMethod = method;
        return this.MainClass.Members.Add(this.MainMethod);
    }
}

如果我们查看列表 2,生成的 C# 源代码,列表 5 中的 Create() 方法应该会产生类似 C# 语句 public void MapEntity(EntityMapper.Model.SalesOrder source, EntityMapper.Model.Invoice target) { }。方法体中没有语句,这是预期的,因为我们仍然需要解析用户输入的业务规则(或来自其他输入源),然后才能确定如何生成方法体。尽管如此,方法体应包含反映我们尚待处理的业务规则的语句。

// -------------------------------------------------------------------
// Listing 5.
// -------------------------------------------------------------------
public class MapCodeMemberMethod
{
    public CodeMemberMethod Create(Type fromType, Type toType, 
      string name = "Map", string fromParamName = "source", string toParamName = "target")
    {
        // Declaring a method  void Map(fromType X, toType) ;
        CodeMemberMethod method = new CodeMemberMethod();
        method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
        method.Name = name;

        // Declares parameters 
        CodeParameterDeclarationExpression paramFromType = 
          new CodeParameterDeclarationExpression(new CodeTypeReference(fromType), fromParamName);
        paramFromType.Direction = FieldDirection.In;
        method.Parameters.Add(paramFromType);

        CodeParameterDeclarationExpression paramToType = 
          new CodeParameterDeclarationExpression(new CodeTypeReference(toType), toParamName);
        paramToType.Direction = FieldDirection.In;
        method.Parameters.Add(paramToType);

        method.ReturnType = new CodeTypeReference("System.Void");

        return method;
    }
}

在继续讨论使用 bsn-goldparser 解析业务规则之前,我需要完成关于 CodeDom 编译单元创建的讨论。它实际上位于另一个辅助类 EntityMapperGeneratorGetClassTypeWrapper() 方法中,如下所示。它创建并返回一个新的 ClassTypeWrapper 类型包装器对象,并添加使用前面描述的 MapCodeMemberMethod 类型对象创建的 CodeMemberMethod 引用。ClassTypeWrapper 拥有所有必需的 CodeDom 类型属性,可供 bsn-goldparser 语义操作实现类通过 ExcecutionContext TypeWrapper 属性引用。

// -------------------------------------------------------------------
// Listing 6.
// -------------------------------------------------------------------
public class EntityMapperGenerator
{
//
//  skipped details here  ... 
//
	private ClassTypeWrapper GetClassTypeWrapper(string fromParmName, 
	        string toParmName, string[] importedNamespaces)
	{
	    var classWrapper = new ClassTypeWrapper(
	        this.NamespaceName, this.ClassName, importedNamespaces);
	    classWrapper.AddMainMethod(new MapCodeMemberMethod().Create(
	      this.FromType, this.ToType, this.MethodName, fromParmName, toParmName));
	    return classWrapper;
	}        
}

使用 bsn-goldparser 引擎解析业务规则

现在,我们有了编译好的语言语法表、CodeDom 的空方法体的 CodeMemberMethod 引用,并且假设我们还从某个地方获得了用户输入的业务规则。下一步是在方法体中生成必要的 CodeDom 语句来表示业务规则。

使用 bsn-goldparser 引擎

在我们使用 bsn-goldparser 生成有用的东西之前,我们需要实现输入语法表中定义的所有终端和规则。基本上,我们需要做的就是创建派生自 SemanticToken 的类,并使用 TerminalAttribute 属性标记提供终端实现的类,并使用 RuleAttribute 属性标记提供语法表中定义的规则实现的方法。如下列表所示,多个终端可以映射到一个类,而在本特定实现中,出于显而易见的原因,它不提供任何处理。另外,请注意我们使用 SemanticToken 派生类 TokenBase 作为所有其他 SemanticToken 实现类的基类。

// -------------------------------------------------------------------
// Listing 7.
// -------------------------------------------------------------------
[Terminal("(EOF)")]
[Terminal("(Error)")]
[Terminal("(Whitespace)")]
[Terminal("(")]
[Terminal(")")]
[Terminal(";")]
[Terminal("=")]
[Terminal(".")]
[Terminal(",")]
public class TokenBase : SemanticToken
{
}

bsn-goldparser 解析中使用的上下文

bsn-goldparser 解析中,Context 对象不过是一个用户定义的结构,用于帮助管理用于编译器生成或解释器执行的代码。在 bsn-goldparser 下载附带的示例中,Context 可以是一个相当复杂的结构,它有助于为 REPL 解释器 实现提供执行环境。对于我们创建 CodeCom 程序图的过程,Context 是一个非常简单的结构,它只提供帮助 CodeDom 程序图生成的设施。看列表 8,我们知道有两个常量定义了源和目标参数名称,它们匹配了列表 5MapCodeMemberMethod 类的 Create() 方法生成的 CodeMemberMethod 引用的参数名称。可能更有趣的是类型为 ClassTypeWrapperTypeWrapper 属性。ClassTypeWrapper 类型有一个 MainMethod 属性,该属性被 AssignStatement 类引用,用于将规则实现添加到 MainMethod 主体中。这里 MainMethod 是在解析开始之前在前面部分描述的空方法体的 CodeMemberMethod 引用。

// -------------------------------------------------------------------
// Listing 8.
// -------------------------------------------------------------------
public class ExecutionContext 
{
    public ClassTypeWrapper TypeWrapper { get; private set; }

    public const string FromParamName = "source";
    public const string ToParamName = "target";

    public ExecutionContext(ClassTypeWrapper typeWrapper)
    {
        TypeWrapper = typeWrapper;
    }
}

回顾列表 9 中的 AssignStatement 类及其 AssignStatement 方法,该方法实现了规则 [Rule(@"<Statement> ::= Identifier ~'=' <Expression> ~';'")](注意:~ 表示跳过后面的标记,因为它在此特定实现中不需要任何映射)。重写的 Execute() 方法构建了等效的 CodeDom 赋值语句来设置目标实体属性,并将新创建的 CodeAssignStatement 引用添加到从 ExecutionContext 引用 ctx 传递的 MainMethod 中的 Statements 集合中。请注意,CodeDom 程序图的构建通过与赋值语句右侧表达式映射的适当 RuleAttribute 级联到相应的映射类。

由于在我的语言语法中,赋值语句的左侧只接受一个属性名(使用标识符终端),以指示要通过求值右侧表达式来设置其值的属性,因此它通过使用 CodeArgumentReferenceExpression(目标参数名称) 获取传递到 CodeMemberMethod 引用的目标参数表达式,并使用 CodePropertyReferenceExpression 引用其属性来构建 CodeDom 表达式。同样,目标参数名称是从传递到 AssignmentStatementExecute() 方法的上下文中获取的。因此,上下文携带了重要的信息和引用,让每个支持业务规则解析的对象类都有足够的信息来处理其处理(CodeDom 表达式创建)。

// -------------------------------------------------------------------
// Listing 9.
// -------------------------------------------------------------------
public class AssignStatement : Statement
{
    private readonly Expression _expr;
    private readonly Identifier _propertyId;

    [Rule(@"<Statement> ::= Identifier ~'=' <Expression> ~';'")]
    public AssignStatement(Identifier propertyId, Expression expr)
    {
        _propertyId = propertyId;
        _expr = expr;
    }

    public override void Execute(ExecutionContext ctx)
    {
        var target = new CodeArgumentReferenceExpression(ExecutionContext.ToParamName);         
        var assignmentStmt = new CodeAssignStatement(
            new CodePropertyReferenceExpression(target, _propertyId.GetName(ctx)), _expr.GetValue(ctx));
        ctx.TypeWrapper.MainMethod.Statements.Add(assignmentStmt);
        // Add assignment statement to main method
    }
}

bsn-goldparser 中的语义操作类映射

在上一节中,我已经讨论了如何在解析业务规则到 CodeDom 程序图时,将语义操作类 SemanticToken 映射到领域语言语法的终端规则。在这里,我们来进一步讨论这个话题。当我开发这个示例程序时,我复制了原始的Simple 2 REPL Interpreter 源代码并开始进行修改,以便类发出 CodeDom 表达式而不是立即解释执行。最终,这比我最初想象的要容易。

只需稍作调整,事情就变得简单了。例如,看看下面 Expression 类的定义。GetValue() 虚拟方法不返回 object 值,如原始解释器源代码所示,而是返回 CodeExpression。当我们想到要生成源代码来指定如何从表达式构造中获取值时,返回表示获取值代码的某种结构就变得很自然了。毕竟,当我们将来需要生成源代码时,CodeExpression 是我们想要的通用结构。

// -------------------------------------------------------------------
// Listing 10.
// -------------------------------------------------------------------
public abstract class Expression : TokenBase
{
    public abstract CodeExpression GetValue(ExecutionContext ctx);
}

如果您查看下面的列表,了解 CodeDom 二元运算表达式的构建(只显示加法运算,其他类似),您肯定会确信构建生成 CodeExpression 的类并不难。您可能会担心操作数与二元运算之间的类型转换。这由 DecimalValueStringLiteral 映射的语义类正确返回的 CodeExpression 来处理,它们基本上会将构造函数中传入的字符串转换为正确的数据类型,然后再使用 CodePrimitiveExpression 返回正确的 CodeDom表达式。在此示例程序中,我没有在语法中添加日期布尔值文字。但是,一旦您理解了我下面如何实现 CodeExpression 生成的想法,添加它们就很简单。

// -------------------------------------------------------------------
// Listing 11.
// -------------------------------------------------------------------
public abstract class BinaryOperator : TokenBase
{
    public abstract CodeBinaryOperatorExpression Evaluate(
           CodeExpression left, CodeExpression right);
}

[Terminal("+")]
public class PlusOperator : BinaryOperator
{
    public override CodeBinaryOperatorExpression Evaluate(CodeExpression left, CodeExpression right)
    {
        return new CodeBinaryOperatorExpression(left, CodeBinaryOperatorType.Add, right);
    }
}

public class BinaryOperation : Expression{
    private readonly Expression _left;
    private readonly BinaryOperator _op;
    private readonly Expression _right;

    [Rule(@"<Expression> ::= <Expression> '>' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '<' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '<=' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '>=' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '==' <Add Exp>")]
    [Rule(@"<Expression> ::= <Expression> '<>' <Add Exp>")]
    [Rule(@"<Add Exp> ::= <Add Exp> '+' <Mult Exp>")]
    [Rule(@"<Add Exp> ::= <Add Exp> '-' <Mult Exp>")]
    [Rule(@"<Mult Exp> ::= <Mult Exp> '*' <Negate Exp>")]
    [Rule(@"<Mult Exp> ::= <Mult Exp> '/' <Negate Exp>")]
    public BinaryOperation(Expression left, BinaryOperator op, Expression right){
        _left = left;
        _op = op;
        _right = right;
    }

    public override CodeExpression GetValue(ExecutionContext ctx)
    {
        CodeExpression lStart = _left.GetValue(ctx);
        CodeExpression rStart = _right.GetValue(ctx);
        return _op.Evaluate(lStart, rStart);
    }
}

[Terminal("DecimalValue")]
public class DecimalValue : Expression
{
    private readonly CodeExpression _value;

    public DecimalValue(string value)
    {
        int intValue;
        if (int.TryParse(value, out intValue)) 
        {
            _value = new CodePrimitiveExpression(intValue);
        }
        else {
            _value = new CodePrimitiveExpression(Convert.ToDecimal(value));
        }
    }

    public override CodeExpression GetValue(ExecutionContext ctx)
    {
        return _value;
    }
}

[Terminal("StringLiteral")]
public class StringLiteral : Expression
{
    private readonly CodeExpression _value;
    public StringLiteral(string value) 
    {
        string trimmedValue = value.Substring(1, value.Length - 2);
        _value = new CodePrimitiveExpression(trimmedValue);
    }

    public override CodeExpression GetValue(ExecutionContext ctx) 
    {
        return _value;
    }
}

对于 <Member Access> 规则,我们需要区分表达式(例如,("ABC" + "XYZ).Length)、源和目标参数(例如,source.InvoiceNo)以及类类型访问(例如,DateTime.Today)。下面的 MemberAccess 类通过映射每个区分规则到重载构造函数来克服这个困难。<Method Access> 规则的实现类似,您可以查看源代码下载进行回顾。我相信通过查看下载的源代码中所有终端和规则实现类,您将能在最短的时间内理解 CodeDom 程序图的生成。

// -------------------------------------------------------------------
// Listing 12.
// -------------------------------------------------------------------
public class MemberAccess : Expression
{
    private readonly Expression _entity;
    private readonly Identifier _member;
    private readonly Identifier _ownerId;

    [Rule(@"<Member Access> ::= <SourceTarget> ~'.' Identifier")]
    public MemberAccess(Expression entity, Identifier member)
    {
        _entity = entity;
        _member = member;
    }

    [Rule(@"<Member Access> ::= Identifier ~'.' Identifier")]
    public MemberAccess(Identifier ownerId, Identifier member)
    {
        _ownerId = ownerId;
        _member = member;
    }

    [Rule(@"<Member Access> ::= <Value> ~'.' Identifier")]
    public MemberAccess(ValueToken val, Identifier member)
    {
        _entity = val;
        _member = member;
    }

    public override CodeExpression GetValue(ExecutionContext ctx)
    {
        if (_entity != null)
        {
            return new CodePropertyReferenceExpression(_entity.GetValue(ctx), _member.GetName(ctx));
        }
        else
        {
            // Type.Property 
            return new CodePropertyReferenceExpression(
              new CodeTypeReferenceExpression(_ownerId.GetName(ctx)), _member.GetName(ctx));
        }
    }
}

源代码生成和编译

EntityMapperGenerator - 管理源代码和程序集生成的类

负责 C# 源代码生成和编译的类是 EntityMapperGenerator。我们在讨论使用 GetClassTypeWrapper() 方法为在传递给语法操作类之前准备 CodeDom 编译单元包装器对象时,已经介绍了它的一个方法(列表 6)。在本节中,我们将讨论它的一些其他方法。

计算哈希以检测业务规则更改

EntityMapperGenerator 中的 Parse() 方法接受业务规则作为字符串文本,并将其传递给 SemanticProcessor 进行解析。在使用 CodeDom 生成动态加载的程序集时有一个问题,即如果程序集正在同一个主 .NET 应用程序域中运行,则无法卸载已加载的程序集。当然,如果您在单独的应用程序域中加载程序集,您可以卸载包含正在运行的程序集的应用程序域而没有任何问题。但会产生另一个问题:要调用另一个应用程序域中的方法,您需要使用类似远程处理的代理对象。这不仅会降低性能,还会导致额外的调用代码,这些代码并不易读(不易读意味着源代码与要解决的当前问题没有直接关系)。

为了最大限度地减少为相同业务规则编译和加载多个程序集的影响,我建立了一个内部字典来跟踪已加载的程序集,该字典使用通过哈希化源类型和目标类型名称与业务规则的连接而计算出的键。对于每条业务规则,我计算哈希集,将其设置为变量 cuKey。下次将相同的 businessRules 输入字符串用于解析时,如果自上次解析和编译以来没有任何变化,则不会创建新的程序集。这减少了动态加载的程序集数量,并通过返回相同业务规则的已加载程序集引用来提高整体性能,同时减少了运行代码大小。整个概念可以用下图总结。

Hash checking before assembly creation

绑定到程序集中已编译方法的委托

成功调用 Parse() 方法后,将创建 CodeCompileUnit 类型的 compileUnit 引用,下一步是编译到程序集。GetDelegate() 方法调用 CodeCompilerUtility 类中的 GetDelegateFromCompileUnit() 方法,该方法将返回包装已编译方法的委托。如果您查看 GetDelegateFromCompileUnit() 的源代码,它会使用传入的哈希 ccuKey 来首先在其中内部字典中查找程序集引用,然后再决定是否需要编译。返回的委托可用于根据传入用于解析的业务规则来处理业务实体。

// -------------------------------------------------------------------
// Listing 13.
// -------------------------------------------------------------------
public class EntityMapperGenerator
{
    public string GrammarTable { get; set; }
    public string NamespaceName { get; private set; }
    public string ClassName { get; private set; }
    public string MethodName { get; private set; }
    public string SourceCode { get; private set; }
    public Type FromType { get; private set; }
    public Type ToType { get; private set; }
    
    private CodeCompileUnit _compileunit;
    private string _cuKey = null;

    public EntityMapperGenerator(string className = "MapperUtility", string methodName = "MapEntity")
    {
        this.GrammarTable = "EntityTransformation.cgt";
        this.NamespaceName = this.GetType().Namespace;
        this.ClassName = className;
        this.MethodName = methodName;
    }

    public void Parse(Type fromType, Type toType, string businessRules, string[] importedNamespaces)
    {
        string fullBusinessRules = string.Format("{0}|{1}|{2}", 
               fromType.FullName, toType.FullName, businessRules);
        string cuKey = Convert.ToBase64String(
          System.Security.Cryptography.HashAlgorithm.Create().ComputeHash(
          System.Text.Encoding.UTF8.GetBytes(fullBusinessRules)));
        if (_cuKey != cuKey)
        {
            this.FromType = fromType;
            this.ToType = toType;
            CompiledGrammar grammar = CompiledGrammar.Load(typeof(TokenBase), this.GrammarTable);
            SemanticTypeActions<TokenBase> actions = new SemanticTypeActions<TokenBase>(grammar);
            actions.Initialize(true);
            SemanticProcessor<TokenBase> processor = 
              new SemanticProcessor<TokenBase>(new StringReader(businessRules), actions);
            ParseMessage parseMessage = processor.ParseAll();
            if (parseMessage == ParseMessage.Accept)
            {
                var ctx = new ExecutionContext(GetClassTypeWrapper(
                    ExecutionContext.FromParamName, ExecutionContext.ToParamName, importedNamespaces));
                var stmts = processor.CurrentToken as Sequence<Statement>;
                foreach (Statement stmt in stmts)
                {
                    stmt.Execute(ctx);
                }
                _compileunit = ctx.TypeWrapper.CompileUnit;
                SourceCode = CodeCompilerUtility.GenerateCSharpCode(_compileunit);
                _cuKey = cuKey;
            }
            else
            {
                IToken token = processor.CurrentToken;
                throw new ApplicationException(string.Format("{0} at line {1} and column {2}", 
                      parseMessage, token.Position.Line, token.Position.Column));
            }
        }
    }

    public Delegate GetDelegate(params string[] referencedAssemblies)
    {
        if (_cuKey == null || _compileunit == null)
        {
            throw new InvalidOperationException("Parse operation is not performed or succeeded!");
        }

        string typeName = this.NamespaceName + "." + this.ClassName;

        Type delType = typeof(Action<,>).MakeGenericType(this.FromType, this.ToType);

        //Get delegate from assembly produced from CodeDom compilation unit
        var mapper = CodeCompilerUtility.GetDelegateFromCompileUnit(
            _cuKey,
            _compileunit,
            referencedAssemblies,
            typeName,
            this.MethodName,
            delType,
            false);

        return mapper;
    }

    private ClassTypeWrapper GetClassTypeWrapper(string fromParmName, 
            string toParmName, string[] importedNamespaces)
    {
        var classWrapper = new ClassTypeWrapper(this.NamespaceName, this.ClassName, importedNamespaces);
        classWrapper.AddMainMethod(new MapCodeMemberMethod().Create(this.FromType, 
                     this.ToType, this.MethodName, fromParmName, toParmName));
        return classWrapper;
    }
}


public class CodeCompilerUtility
{
	// Skipped lines ...

    public static Delegate GetDelegateFromCompileUnit(string ccuKey, 
           CodeCompileUnit compileunit, string[] referencedAssemblies, 
           string typeName, string methodName, 
           Type delegateType, bool refreshCache = false) 
    {
        Assembly assembly;
        if (!Assemblies.ContainsKey(ccuKey) || refreshCache)
        {
            assembly = CompileCodeDOM(compileunit, referencedAssemblies);
            if (Assemblies.ContainsKey(ccuKey)) {
                Assemblies.Remove(ccuKey); 
            }
            Assemblies.Add(ccuKey, assembly);
        }
        else
        {
            assembly = Assemblies[ccuKey];
        }

        var type = assembly.GetType(typeName, true);
        var method = type.GetMethod(methodName);
        var obj = assembly.CreateInstance(typeName);
        return Delegate.CreateDelegate(delegateType, obj, method);
    }
}

摘要

通过定义 BNF 语法的领域语言并使用解析器工具(GOLD Parser)、引擎(bsn-goldparser)和程序集生成类来实现业务规则引擎的创建,起初似乎是不必要的。但长远来看,您的工作将通过为您应用程序提供最大的灵活性来适应业务最苛刻的需求变化而得到回报。

历史

  • 2012.04.05: 版本 1.0 和文档创建。
  • 2012.04.07: 版本 1.1 和文档更新。
© . All rights reserved.