XslCompiledTransform和自定义XmlUrlResolver:“具有相同键的条目已存在”

有没有办法调试由自定义XmlUrlResolver从数据库加载的XSLT文档,或者有人知道,下面的错误消息是什么?

我有一个XSLT样式表导入一个常见的xslt文档:

 

Scheme由一个自定义的XmlResolver处理,它从数据库加载XSLT文档,但是我收到一个错误:

已存在具有相同密钥的条目。

xsl:import引用的公共XSLT文档包含一些常见的XSLT模板,每个模板都有一个唯一的名称。

将XSLT文档从本地文件系统移动到数据库后,此错误开始发生。 使用指向本地文件的默认导入方案时以及从本地文件系统加载XSLT文档时,不会发生错误。

我还尝试在创建XslCompiledTransform的实例时打开调试,但不知何故不可能“进入”基于数据库的XSLT。

 _xslHtmlOutput = new XslCompiledTransform(XSLT_DEBUG); 

更新:以下基本上是请求的解析器​​代码,但我的代码中没有发生exception; 因此,我想在下面的代码中没有明显的原因。 (这个相同的代码实际上用于加载包含导入的XSLT样式表,当注释掉导入时,一切都按预期工作。)

 public class XmlDBResolver : XmlUrlResolver { private IDictionary GetUriComponents(String uri) { bool useXmlPre = false; uri = uri.Replace("db://", ""); useXmlPre = uri.StartsWith("xml/"); uri = uri.Replace("xml/", ""); IDictionary dict = new Dictionary(); string app = null, area = null, subArea = null; if (!String.IsNullOrWhiteSpace(uri)) { string[] components = uri.Split('.'); if (components == null) throw new Exception("Invalid Xslt URI"); switch (components.Count()) { case 3: app = components[0]; break; case 4: area = components[0]; app = components[1]; break; case 5: subArea = components[0]; area = components[1]; app = components[2]; break; default: throw new Exception("Invalid Xslt URI"); } dict.Add("application", app); dict.Add("area", area); dict.Add("subArea", subArea); dict.Add("xmlPreTransform", String.Format("{0}", useXmlPre)); } return dict; } public override System.Net.ICredentials Credentials { set { /* TODO: check if we need credentials */ } } public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn) { /* * db://.hist.org * db://..hist.org * db://...hist.org * * */ Tracing.TraceHelper.WriteLine(String.Format("GetEntity {0}", absoluteUri)); XmlReader reader = null; switch (absoluteUri.Scheme) { case "db": string origString = absoluteUri.OriginalString; IDictionary xsltDict = GetUriComponents(origString); if(String.IsNullOrWhiteSpace(xsltDict["area"])) { reader = DatabaseServiceFactory.DatabaseService.GetApplicationXslt(xsltDict["application"]); } else if (!String.IsNullOrWhiteSpace(xsltDict["area"]) && String.IsNullOrWhiteSpace(xsltDict["subArea"]) && !Boolean.Parse(xsltDict["xmlPreTransform"])) { reader = DatabaseServiceFactory.DatabaseService.GetAreaXslt(xsltDict["application"], xsltDict["area"]); } else if (!String.IsNullOrWhiteSpace(xsltDict["area"]) && !String.IsNullOrWhiteSpace(xsltDict["subArea"])) { if(Boolean.Parse(xsltDict["xmlPreTransform"])) reader = DatabaseServiceFactory.DatabaseService.GetSubareaXmlPreTransformXslt(xsltDict["application"], xsltDict["area"], xsltDict["subArea"]); else reader = DatabaseServiceFactory.DatabaseService.GetSubareaXslt(xsltDict["application"], xsltDict["area"], xsltDict["subArea"]); } return reader; default: return base.GetEntity(absoluteUri, role, ofObjectToReturn); } } 

为了完整性,IDatabaseService接口(相关部分):

 public interface IDatabaseService { ... XmlReader GetApplicationXslt(String applicationName); XmlReader GetAreaXslt(String applicationName, String areaName); XmlReader GetSubareaXslt(String applicationName, String areaName, String subAreaName); XmlReader GetSubareaXmlPreTransformXslt(String applicationName, String areaName, String subAreaName); } 

更新:我试图通过从Web服务器临时加载样式表来隔离问题,这是有效的。 我了解到SQL Server显然只存储没有XML声明的XML片段,而不是存储在Web服务器上的样式表。

更新:exception的堆栈跟踪:

System.Xml.Xsl.XslLoadException:XSLT-Kompilierungsfehler。 Fehler bei(9,1616)。 —> System.ArgumentException:具有相同键的条目已存在.bei System.Collections.Specialized.ListDictionary.Add(Object key,Object value)bei System.Collections.Specialized.HybridDictionary.Add(Object key,Object值)System System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(XmlReader reader,Boolean include)bei System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(Uri uri,Boolean include)bei System.Xml.Xsl.Xslt.XsltLoader .LoadStylesheet(XmlReader reader,Boolean include)— Ende der inneren Ablaufverfolgung des Ausnahmestacks — bei System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(XmlReader reader,Boolean include)bei System.Xml.Xsl.Xslt.XsltLoader .Load(XmlReader reader)bei System.Xml.Xsl.Xslt.XsltLoader.Load(Compiler compiler,Object stylesheet,XmlResolver xmlResolver)bei System.Xml.Xsl.Xslt.Compiler.Compile(Object stylesheet,XmlResolver xmlResolver,QilExpression&qil) bei System.Xml.Xsl.XslCompiledTransform.LoadInternal(对象样式表,XsltSettings设置,XmlResolve r stylesheetResolver)bei System.Xml.Xsl.XslCompiledTransform.Load(String stylesheetUri,XsltSettings settings,XmlResolver stylesheetResolver)bei(my namespace and class).GetXslTransform(Boolean preTransform)bei(my namespace and class).get_XslHtmlOutput()bei(my命名空间和类).get_DisplayMarkup()

简短回答:

您的IDatabaseService接口方法返回XmlReader对象。 构造它们时,请确保将baseUri传递给构造函数; 例如:

 public XmlReader GetApplicationXslt(string applicationName) { … var baseUri = string.Format("db://{0}.hist.org", applicationName); return XmlReader.Create(input: …, settings: …, baseUri: baseUri); // <-- this one is important! } 

如果指定此参数,一切都可能正常工作。 请参阅本答案的最后一部分,看看为什么我建议这样做。


答案很长,简介:可能的错误来源:

让我们首先简要地思考哪些组件可能导致错误:

“将XSLT文档从本地文件系统移动到数据库后,就开始出现此错误。使用指向本地文件的默认导入方案时,以及从本地文件系统加载XSLT文档时,不会发生错误。”

将样式表放在数据库中意味着你必须拥有......

  1. 更改了样式表中的导入路径(介绍了db://… paths)
  2. 实现并连接自定义XmlDbResolver以处理db:// import方案
  3. XmlDbResolver的forms实现数据库访问代码,后者支持XmlDbResolver

如果样式表除了导入路径之外没有变化,那么错误可能在XmlResolver类和/或IDatabaseService实现中。 由于您没有显示后者的代码,因此我们无法在没有猜测的情况下调试您的代码。

我使用您的XmlDbResolver创建了一个模拟项目(完整描述如下)。 我无法重现错误,因此我怀疑您的IDatabaseService实现会导致错误。

更新:我已经能够重现错误。 请参阅OP的评论和本答案的最后一部分。


我尝试重现您的错误:

我在Visual Studio 2010中创建了一个控制台应用程序项目(您可以通过使用Git克隆此Gist来检索( git clone https://gist.github.com/fbbd5e7319bd6c281c50b4ebb1cee1f9.git ),然后检查第二次提交, git checkout d00629 )。 我将在下面更详细地描述每个解决方案的项目。

项目项目

(请注意, SqlServerDatabase.mdfTestInput.xml.xslt项目项的“ 复制到输出目录”属性应设置为“ 始终” 。)


SqlServerDatabase.mdf:

这是一个基于服务的数据库,我将附加到SQL Server Express 2008的本地实例。(这是通过App.config的连接字符串完成的;请参阅下文。)

我在这个数据库中设置了以下项目:

SqlServerDatabase结构

该表包含两列,定义如下:

ApplicationDocuments列定义

这些表最初是空的。 测试数据将在运行时添加到数据库中(请参阅下面的Program.csCommonHistOrg.xslt )。


App.config中:

此文件包含上述数据库的连接字符串条目。

       

IDatabaseService.cs:

该文件包含IDatabaseService接口的定义,我在此不再赘述。


SqlServerDatabaseService.cs:

这包含一个实现IDatabaseService的类。 它读/写数据到上面的数据库:

 using System; using System.Collections.Generic; using System.Configuration; using System.Data.SqlClient; using System.Data.SqlTypes; using System.IO; using System.Xml; class SqlServerDatabaseService : IDatabaseService { // creates a connection based on connection string from App.config: SqlConnection CreateConnection() { return new SqlConnection(connectionString: ConfigurationManager.ConnectionStrings["SqlServerDatabase"].ConnectionString); } // stores an XML document into the 'ApplicationDocuments' table: public void StoreApplicationDocument(string applicationName, XmlReader document) { using (var connection = CreateConnection()) { SqlCommand command = connection.CreateCommand(); command.CommandText = "INSERT INTO ApplicationDocuments (ApplicationName, Document) VALUES (@applicationName, @document)"; command.Parameters.Add(new SqlParameter("@applicationName", applicationName)); command.Parameters.Add(new SqlParameter("@document", new SqlXml(document))); // ^^^^^^^^^^^^^^^^^^^^ connection.Open(); int numberOfRowsInserted = command.ExecuteNonQuery(); connection.Close(); } } // reads an XML document from the 'ApplicationDocuments' table: public XmlReader GetApplicationXslt(string applicationName) { using (var connection = CreateConnection()) { SqlCommand command = connection.CreateCommand(); command.CommandText = "SELECT Document FROM ApplicationDocuments WHERE ApplicationName = @applicationName"; command.Parameters.Add(new SqlParameter("@applicationName", applicationName)); connection.Open(); var plainXml = (string)command.ExecuteScalar(); connection.Close(); if (plainXml != null) { return XmlReader.Create(new StringReader(plainXml)); } else { throw new KeyNotFoundException(message: string.Format("Database does not contain a application document named '{0}'.", applicationName)); } } } … // (all other methods throw a NotImplementedException) } 

XmlDbResolver.cs:

它包含XmlDbResolver类,除了两个更改外,它与XmlDBResolver类相同:

  1. 公共构造函数接受IDatabaseService对象。 这用于代替DatabaseServiceFactory.DatabaseService

  2. 我不得不删除对Tracing.TraceHelper.WriteLine的调用。


CommonHistOrg.xslt:

这是db://common.hist.org样式表,它将在运行时放入数据库中(参见下面的Program.cs ):

       

TestStylesheet.xml:

这是一个参考上面的db://common.hist.org样式表的样式表:

     

TestInput.xml:

这是我们将使用上面的TestStylesheet.xslt转换的XML测试输入文档:

   

Program.cs中:

这包含测试应用程序代码:

 using System; using System.Text; using System.Xml; using System.Xml.Xsl; class Program { static void Main(string[] args) { var databaseService = new SqlServerDatabaseService(); // put CommonHistOrg.xslt into the 'ApplicationDocuments' database table: databaseService.StoreApplicationDocument( applicationName: "common", document: XmlReader.Create("CommonHistOrg.xslt")); // load the XSLT stylesheet: var xslt = new XslCompiledTransform(); xslt.Load(@"TestStylesheet.xslt", settings: XsltSettings.Default, stylesheetResolver: new XmlDbResolver(databaseService)); // load the XML test input: var input = XmlReader.Create("TestInput.xml"); // transform the test input and store the result in 'output': var output = new StringBuilder(); xslt.Transform(input, XmlWriter.Create(output)); // display the transformed output: Console.WriteLine(output.ToString()); Console.ReadLine(); } } 

在我的机器上像魅力一样工作:输出是一个带有空根元素的XML文档,这是db://common.hist.org样式表从测试中输出匹配的元素的内容输入。


更新:错误再现和修复:

  1. Main方法中插入以下语句:

     databaseService.StoreApplicationDocument( applicationName: "test", document: XmlReader.Create("TestStylesheet.xslt")); 
  2. 代替

     xslt.Load(@"TestStylesheet.xslt", …); 

     xslt.Load(@"db://test.hist.org", …); 

    这会触发OP报告的错误。

经过一些调试后,我发现以下内容不会导致此问题。

  • 数据库表中的Document列具有XML类型的事实。 它也失败了NTEXT

  • 事实上,从DB返回的文档中缺少标头。 即使在SqlServerDatabaseService将控制权返回给框架之前手动添加XML标头,错误仍然存​​在。

实际上,错误是在.NET Framework代码中的某处触发的。 这就是我决定下载并安装.NET Framework参考源的原因 。 (我将解决方案更改为使用框架版本3.5进行调试。)安装此项并重新启动VS然后允许您在调试会话期间查看并逐步执行框架代码。

从我们的Main方法中调用xslt.Load(…;) ,我进入框架代码并最终找到了LoadStylesheet的方法XsltLoader.cs 。 有一个名为documentUrisInUseHybridDictionary ,它显然存储已经加载的样式表的基URI。 因此,如果我们加载多个带有空URI或缺少基URI的样式表,则此方法将尝试将该字典添加两次null ; 这就是导致错误的原因。

因此,一旦为IDatabaseService返回的每个样式表分配了唯一的基本URI,一切都应该可以正常工作。 您可以通过将baseUri传递给XmlReader构造函数来完成此操作。 在我的答案的最开始看一个代码示例。 您还可以通过下载或克隆此Gist来检索更新的,有效的解决方案( git clone https://gist.github.com/fbbd5e7319bd6c281c50b4ebb1cee1f9.git )。