一个单文件函数解析器,用于 WPF、Silverlight 和 SQL Server CLR
一个轻量级的单文件函数解析器,使用类似 Excel 的语法。

引言
前一段时间,我在处理经典的三层架构时遇到一个有趣的问题。我发现每一层都非常相似地需要动态地扩展 XML 和 HTML 数据,用来自数据库和客户输入中的变量和计算值。这自然而然地引出了使用类似数学解析器的想法。理想情况下,这个解析器应该能够处理像“IIF([var1] > 5, ‘OK’, ‘To Low’)”这样的表达式,以便普通 **Excel** 用户能够轻松理解和使用。
“没问题,”我想,“我一定能在我最喜欢的编程门户 CodeProject 上找到一个简洁且现成的解决方案,”但是……
在花了一些时间测试了三个非常有前景的解决方案的代码后,我总是遇到同样的几个关键问题。
- 我的要求是,在所有层中**精确地**使用相同的代码,以确保应用程序的**所有**部分行为一致,而无需在进行小的更改后反复测试。这意味着相同的代码应该在不作任何修改的情况下,适用于 SQL Server CLR、WPF 服务、WPF 客户端和 Silverlight 客户端。
- 我不想使用一个完整的解析“框架”或额外的程序集,而是想要一个小巧的解决方案,最好是单个文件。
将现有解决方案改编以满足这些要求非常耗时。因此,尤其因为不需要一个功能齐全的数学解析器,我决定编写自己的简单函数解析器,并与社区分享我的经验。
背景
此实现的目的是:
- 将**所有**功能保留在**单个文件**中,以便在不同项目之间轻松共享。
- 完全兼容 **WPF**、**Silverlight** 和 **SQL Server CLR** 应用程序。
- 使用简单的函数语法,这样具有 **EXCEL** 技能的普通用户就能理解。
- 能够轻松地使用**XML 路径查询**。
此实现的**目的不是**:
- 成为一个功能齐全的**数学解析器**。
- 追求解析数百万行数据的**速度记录**。
- 用其**复杂性**和**大量的函数**给人留下深刻印象。
使用代码
您需要做的就是将 *FunctionParser.cs* 添加到您的项目中,所以让我们从一个简单的例子开始。
手动添加变量
创建一个字典
Dictionary<string, object> variables = new Dictionary<string, object>();
添加一些值
variables.Add("doubleVar1", 3.0);
variables.Add("doubleVar2", 7.5);
然后就可以高兴地使用了
string result = FunctionParser.Parse("The sum of [doubleVar1] and " + 
      "[doubleVar2] is {SUM([doubleVar1], [doubleVar2])}\n",  variables);
您将获得字符串“[doubleVar1] 和 [doubleVar2] 的和是 10.5”。
使用 XML 数据 (1)
现在我们可以尝试一个更复杂的例子了。假设我们有一些来自数据库的 XML 数据存储在一个名为 `customData` 的 `string` 变量中,我们想用它来填写一个表单信件。
<Columns>
    <Contact>
        <Column Id="Gender">Female</Column>
        <Column Id="FirstName">Carol</Column>
        <Column Id="LastName">Holland</Column>
    </Contact>
    <Address>
        <Column Id="StreetNumber">456</Column>
        <Column Id="Street">School Road</Column>
        <Column Id="ZIP">GA 50001</Column>
        <Column Id="City">Marietta</Column>
    </Address>
    <Company>
        <Column Id="Position">Director of Education</Column>
        <Column Id="Name">The Wontimal School</Column>
    </Company>
</Columns>
那么我们如何让它与我们的函数解析器一起工作呢?为了简化,有一个名为 `XmlVariableContainer` 的类。
XmlVariableContainer container = new XmlVariableContainer(customData, "//Columns//Column", true);
第一个参数是我们的 XML 数据。用作变量的元素必须有一个 Id 属性,因为我们需要一个变量键,但它们不必一定称为“columns”。第二个参数是一个 XPath 查询,用于枚举这些元素。最后一个参数决定父节点名称是否也将用作键的一部分。如果此参数设置为 `true`,我们可以使用 `[Parent.child]` 符号访问生成的变量,这样文本的可读性更强。
`XmlVariableContainer` 使用一个简单的 `Dictionary
string firstName = container.Variables["Contact.FirstName"];
现在我们可以再次轻松地使用解析器了。
string sampleLetter = File.ReadAllText(@"Debug\Form Letter Example\SampleLetter.html");
string result = FunctionParser.Parse(sampleLetter, container.GetValue); 
使用 XML 数据 (2)
有时 `<column Id=“..“ >` 符号很不实用,例如当您想从数据库中提取 XML 数据时(参见“SQL Server CLR 示例”)。在这种情况下,我们也可以使用以下符号:
<Data>
    <Contact>
        <Gender>Male</Gender>
        <FirstName>Carol</FirstName>
        <LastName>Holland</LastName>
    </Contact>
    <Address>
        <StreetNumber>456</StreetNumber>
        <Street>School Road</Street>
        <ZIP>GA 50001</ZIP>
        <City>Marietta</City>
    </Address>
    <Company>
        <Position>Director of Education</Position>
        <Name>The Wontimal School</Name>
    </Company>
</Data>
要获得与上一个示例(*“使用 XML 数据 1”*)相同的结果,我们需要稍微改变 `XmlVariableContainer` 的构造。
XmlVariableContainer container = new XmlVariableContainer(customData, "Data//*//*", true);
使用带 XML 变量内容的 XML 数据
到目前为止,我们讨论的是通过 XML 生成字符串或数字等变量,但如果我们需要的变量内容是 XML 数据呢?
<Variables>
    <Column Id="DoubleValue1">12.33</Column>
    <Column Id="DoubleValue2">0.5</Column>
    <Column Id="XmlValue" Type="xml">
        <Rows>
            <Row>
                <Column Id="Gender">Female</Column>
                <Column Id="FirstName">Carol</Column>
                <Column Id="LastName">Holland</Column>
                <Column Id="StreetNumber">456</Column>
                <Column Id="Street">School Road</Column>
                <Column Id="ZIP">GA 50001</Column>
                <Column Id="City">Marietta</Column>
            </Row>
            <Row>
                <Column Id="Gender">Male</Column>
                <Column Id="FirstName">John</Column>
                <Column Id="LastName">James</Column>
                <Column Id="StreetNumber">22</Column>
                <Column Id="Street">Maple Street</Column>
                <Column Id="ZIP">11111</Column>
                <Column Id="City">Independence</Column>
            </Row>
        </Rows>
    </Column>
</Variables>
要生成 XML 内容而不是文本或数字,只需添加 `Type="xml"` 属性。这个变量现在可以轻松地用作 XML 函数 `XQUERY()` 和 `XVALUE()` 的参数。
XVALUE([XmlValue], 'Rows/Row/Column[@Id=\"FirstName\"]/text()')
这会导致字符串数组 {„Carol“, „John“}。
SQL Server CLR 示例
该解析器可以轻松地与 `XmlVariableContainer` 类一起使用,创建一个简单的评估函数。
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlChars Evaluate(SqlChars text, SqlXml columns, 
       SqlString columnPath, SqlBoolean useParentIdentifier)
{
    if (!text.IsNull && !columns.IsNull && !columnPath.IsNull)
    {
        bool useIdentifier = useParentIdentifier.IsNull || useParentIdentifier.IsFalse ? false : true;
        XmlVariableContainer container = new XmlVariableContainer();
        XDocument columnsDocument = XDocument.Load(columns.CreateReader(), LoadOptions.None);
        container.AddColumns(columnsDocument.XPathSelectElements(columnPath.ToString()), useIdentifier);
        return new SqlChars(FunctionParser.Parse(text.ToSqlString().ToString(), 
                            container.GetValue).ToCharArray());
    }
    return new SqlChars();
}
假设我们有一个名为“Contact”的表,至少包含 [Salutation]、[FirstName] 和 [LastName] 列,我们可以使用 FOR XML 语法创建所需的 XML 数据。
declare @Contacts xml = 
    (SELECT
        [Salutation]
        ,[FirstName]
        ,[LastName]
    FROM
        [Data].[Contact]
    FOR XML PATH('Contact'), ROOT('Contacts'))
现在我们可以调用先前创建的 Sql 函数。
declare @formLetter nvarchar(max) = '...'
SELECT [Common].[dbo].[Evaluate](@formLetter, @Contacts, 'Contacts/Contact[1]/*', 0)
您将在演示应用程序启动时找到更多示例和完整的参考文档。
添加新函数
我试图让添加新函数尽可能容易。所以,假设我们需要一个像 ISEMPTY() 这样的函数,它用来判断传入的参数是 null 还是空。这可以通过将以下代码添加到 `FunctionParser.Function` 类来实现。
// class FunctionParser. Function
/* Public Evaluation Methods */
...
[ParserFunction]
public bool ISEMPTY()
{
    if (this.Parameters.Length == 1)
    {
        this.Value = string.IsNullOrEmpty(this.Parameters[0].StringValue);
        return true;
    }
    return false;
}
所有解析器函数都用 `[ParserFunctionAttribute]` 标记,如果解析成功则返回 `true`,否则返回 `false`。可以通过 `Parameters` 数组访问参数。返回值可以通过赋值 `Value` 属性来设置(这有点像 VBA 语法)。
在我们的例子中,实现仅限于检查 `Parameters` 的长度(应该正好是 1),并在相应字符串为 null 或空时将 `Value` 参数设置为 `true`。
历史
- 2013/04/26:发布初始版本。




