最近参与的电子商务系统即将完成,不久前分析师提了一个需求:
在系统上增加一个小的模块用来处理最终用户可以向我们提交在使用系统是遇到的一些问题。这样的我们的Support Team就可以更容易理解客户想说的到底是遇到了什么样的问题。
针对这个需求,我们定义出了下面的实体:
Code
public abstract class WidgetEntityBase
{
private int m_transactionNumber;
public int TransactionNumber
{
get { return m_transactionNumber; }
set { m_transactionNumber = value; }
}
public abstract string Category
{
get;
}
}
public class IssueReport : WidgetEntityBase
{
public override string Category
{
get
{
return "IssueReport";
}
}
private string m_errorCode;
public string ErrorCode
{
get { return m_errorCode; }
set { m_errorCode = value; }
}
private string m_errorDescription;
public string ErrorDescription
{
get { return m_errorDescription; }
set { m_errorDescription = value; }
}
private string m_errorMessage;
public string ErrorMessage
{
get { return m_errorMessage; }
set { m_errorMessage = value; }
}
private string m_submitUserName;
public string SubmitUserName
{
get { return m_submitUserName; }
set { m_submitUserName = value; }
}
private string m_submitUserEmail;
public string SubmitUserEmail
{
get { return m_submitUserEmail; }
set { m_submitUserEmail = value; }
}
private DateTime m_submitDate;
public DateTime SubmitDate
{
get { return m_submitDate; }
set { m_submitDate = value; }
}
private string m_status;
public string Status
{
get { return m_status; }
set { m_status = value; }
}
private string m_resolvedBy;
public string ResolvedBy
{
get { return m_resolvedBy; }
set { m_resolvedBy = value; }
}
private DateTime? m_resolvedDate;
public DateTime? ResolvedDate
{
get { return m_resolvedDate; }
set { m_resolvedDate = value; }
}
private string m_resolutionSumary;
public string ResolutionSumary
{
get { return m_resolutionSumary; }
set { m_resolutionSumary = value; }
}
}
数据库应用嘛,接下来考虑数据库的设计吧。这应该是很简单的,我这里也不唠叨了。然而,这样设计好吗?考虑到变化,后面我们可能还要增加一个小模块,用来处理最终用户对于系统的改进建议等等。这样的话,我们维护这些表会感觉有些不爽。我们可不可以这样设计,一个字段(TransactionNumber)存储编号,一个字段(WidgetCategory)存储分类,一个字段(WidgetContent)存储实际的内容(XML文本)。这样一来,我们就可以很灵活地给系统增加很多比较有用的Widgets.我们将WidgetEntity序列化为的XML文本存储在WidgetContent中,查询时,我们使用XQuery/XPath来查询WidgetContent,然后将过滤得到的WidgetContent反序列化为相应的WidgetEntity.
针对这个需求,我们定义出了相应的查询条件实体:
Code
public class IssueReportQueryCriteria
{
public static string Category
{
get
{
return "IssueReport";
}
}
private string m_errorCode;
[XPathMapping("/IssueReport[ErrorCode=\"{0}\"]")]
public string ErrorCode
{
get { return m_errorCode; }
set { m_errorCode = value; }
}
private string m_errorDescription;
[XPathMapping("/IssueReport/ErrorDescription[fn:contains(.,\"{0}\"]")]
public string ErrorDescription
{
get { return m_errorDescription; }
set { m_errorDescription = value; }
}
private string m_submitUserName;
[XPathMapping("/IssueReport/SubmitUserName[fn:contains(.,\"{0}\"]")]
public string SubmitUserName
{
get { return m_submitUserName; }
set { m_submitUserName = value; }
}
private string m_status;
[XPathMapping("/IssueReport[Status=\"{0}\"]")]
public string Status
{
get { return m_status; }
set { m_status = value; }
}
private DateTime? m_submitDateFrom;
[XPathMapping("/IssueReport[SubmitDate>=\"{0}\"]")]
public DateTime? SubmitDateFrom
{
get { return m_submitDateFrom; }
set { m_submitDateFrom = value; }
}
private DateTime? m_submitDateTo;
[XPathMapping("/IssueReport[SubmitDate<\"{0}\"]")]
public DateTime? SubmitDateTo
{
get { return m_submitDateTo; }
set { m_submitDateTo = value; }
}
private DateTime? m_resolvedDateFrom;
[XPathMapping("/IssueReport[ResolvedDate>=\"{0}\"]")]
public DateTime? ResolvedDateFrom
{
get { return m_resolvedDateFrom; }
set { m_resolvedDateFrom = value; }
}
private DateTime? m_resolvedDateTo;
[XPathMapping("/IssueReport[ResolvedDate<\"{0}\"]")]
public DateTime? ResolvedDateTo
{
get { return m_resolvedDateTo; }
set { m_resolvedDateTo = value; }
}
private string m_resolvedBy;
[XPathMapping("/IssueReport/ResolvedBy[fn:contains(.,\"{0}\"]")]
public string ResolvedBy
{
get { return m_resolvedBy; }
set { m_resolvedBy = value; }
}
}
那么我们如何构建相应的XPath表达式呢?
我可不想一个手动构造。这里我写了一个自定义Attribute来帮助生成相应的XPath表达式:
//XPathMappingAttribute
Code
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class XPathMappingAttribute : Attribute
{
public XPathMappingAttribute(string xPathExpressionFormat)
{
this.m_xPathExpressionFormat = xPathExpressionFormat;
}
private string m_xPathExpressionFormat;
public string XPathExpressionFormat
{
get { return m_xPathExpressionFormat; }
set { m_xPathExpressionFormat = value; }
}
}
XPathMappingAttribute是一个可以应用在属性上的自定义Attribute,它有一个构造参数,就是要应用在这个属性上的XPath表达式的格式化字符串。这样,我们就可以利用反射技术根据属性相应的XPath表达式的格式化字符串以及属性的值来生成整个查询实体对应的XPath表达式。参考下面的代码:
//XPathHelper
Code
public static class XPathHelper
{
public static string GenerateXPathExpression<T>(T xPathCriteria) where T:class
{
if (xPathCriteria == null)
{
return string.Empty;
}
Type xPathCriteriaType = typeof(T);
PropertyInfo[] properties = xPathCriteriaType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetField);
if (properties != null && properties.Length > 0)
{
StringBuilder sbXPathExpression = new StringBuilder(1024);
foreach (PropertyInfo property in properties)
{
object propertyValue = property.GetValue(xPathCriteria, null);
if (propertyValue != null &&!string.IsNullOrEmpty(propertyValue.ToString()))
{
object[] attributes = property.GetCustomAttributes(typeof(XPathMappingAttribute), true);
if (attributes != null && attributes.Length > 0)
{
XPathMappingAttribute xPathMapping = attributes[0] as XPathMappingAttribute;
if (xPathMapping != null)
{
sbXPathExpression.AppendFormat(xPathMapping.XPathExpressionFormat, propertyValue.ToString());
sbXPathExpression.Append(" and ");
}
}
}
}
if (sbXPathExpression.Length > 0)
{
sbXPathExpression.Append("1=1");
}
return sbXPathExpression.ToString();
}
return string.Empty;
}
}
看下单元测试吧:
Code
namespace UnitTest
{
[TestFixture]
public class TestXPathHelper
{
private static IssueReportQueryCriteria s_issueReportXPathCriteria;
[SetUp]
public void SetUp()
{
s_issueReportXPathCriteria = new IssueReportQueryCriteria();
s_issueReportXPathCriteria.ErrorDescription = "description";
s_issueReportXPathCriteria.Status = "状态:A";
s_issueReportXPathCriteria.SubmitDateFrom = DateTime.Now.AddDays(-7);
s_issueReportXPathCriteria.SubmitDateTo = DateTime.Now;
s_issueReportXPathCriteria.ResolvedDateTo = DateTime.Now;
}
[Test]
public void GenerateXPathExpression()
{
string xPathExpression = XPathHelper.GenerateXPathExpression<IssueReportQueryCriteria>(s_issueReportXPathCriteria);
Console.WriteLine("XPathExpression:");
Console.WriteLine(xPathExpression);
}
[Test]
public void SerializeObjectToXML()
{
XmlSerializer serializer = new XmlSerializer(typeof(IssueReportQueryCriteria));
using (MemoryStream ms = new MemoryStream())
{
serializer.Serialize(ms, s_issueReportXPathCriteria);
Console.WriteLine(System.Text.Encoding.UTF8.GetString(ms.ToArray()));
//Close Memory Stream
ms.Close();
}
}
}
}
输出如下:
XPathExpression:
/IssueReport/ErrorDescription[fn:contains(.,"description"] and /IssueReport[Status="状态:A"] and /IssueReport[SubmitDate>="2008-9-16 20:11:25"] and /IssueReport[SubmitDate<"2008-9-23 20:11:25"] and /IssueReport[ResolvedDate<"2008-9-23 20:11:25"] and 1=1
细心的网友一定注意到后面加了一个" and 1=1"为什么要这样加呢?
这是因为在使用WidgetContent.value("XPathExpression","bit")=1时,其中的当XPathExpression只有一个,比如/IssueReport/ErrorDescription[fn:contains(.,"description"] 时,就会报下面的一个异常:
Msg 2389, Level 16, State 1, Line 5
XQuery [dbo.ApplicationWidgets.WidgetContent.value()]: 'value()' requires a singleton (or empty sequence), found operand of type 'xdt:untypedAtomic *'
这是因为value(parm1,parm2)的param1在param2是bit类型时必须是一个逻辑表达式,在这里我们就要为当只有一个查询条件是构造一个加上" and 1=1"的一个逻辑表达式.
并且注意XPathExpression中的and必须为小写,不然会遇到下面的异常:
Msg 2370, Level 16, State 1, Line 6
XQuery [dbo.ApplicationWidgets.WidgetContent.value()]: No more tokens expected at the end of the XQuery expression. Found 'AND'.
诚找合作伙伴或有好的投资项目的老板.呵呵