一个单文件函数解析器,用于 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:发布初始版本。