我正在开发一个遗留的ASP.NET代码库,其中一些信息存储在DataTable
对象内的缓存中(通过企业库)。它是一个在.NET 4.0中运行的多用户Intranet环境。生产中存在一个问题,它指出先前的“固定”问题是可能的根本原因:KeyNotFoundException
发生在DataView.ToTable()
的调用中。此特定代码是在从应用程序中的大多数页面加载页面期间可能发生的验证的一部分。
var table = GetSomeDataTableFromCache();
var view = table.DefaultView;
view.RowFilter = "Foo = 'Bar'";
var filteredTable = view.ToTable();
这是代码的简化。发生的事情是,自从传递了 long 之后,这段代码显然抛出了上述异常。先前的开发人员通过捕获和吞咽异常并返回null来“修复”它。这种行为是其他问题的罪魁祸首。
我试图在没有太大成功的情况下重现原始异常。我觉得如果我可以重现它,我可以理解它并尝试形成一个比忽略它更好的原始问题的解决方案,从而也避免了忽略它的混乱< / em>已创建。
我注意到Google在DataView.ToTable() + KeyNotFoundException
上搜索产生的结果表明验证列名存在。一个例子就在这里:
http://forums.asp.net/t/1695676.aspx/1
但是,我发现在将RowFilter
设置为无效的列名时,该行上会出现EvaluateException
。我还试图通过使用空表以及使用产生空结果的过滤器来解决错误。每个场景都证明是非常例的。
那么KeyNotFoundException
会出现在哪里,如果它不是建议的无效列名?一旦我们知道它是如何发生的,我们怎么能避免呢?
答案 0 :(得分:3)
请注意,ASP.NET本质上是一个多线程系统。记录DataView
对于读取是线程安全的,但对于写入则不是。这一点很重要。
在提供的代码段中,从缓存中提取DataTable
的实例并访问其DefaultView
以进行过滤并写入新的DataTable
,这可能会提供一个可以证明的条件不安全,并不是因为过滤器中的列,而是因为多个线程可能同时执行相同的代码。
作为实现细节,DataView
使用了几位内部状态。在ToTable()
执行期间,涉及多个非本地状态。特别是,有一个键入DataRow的字典字段,以及一个用作键的DataRow字段!这个字典被清除,添加到,行被一个引用一行的值覆盖,然后被设置为null,这是该过程的所有部分。当多个线程同时执行时,一个线程覆盖并使另一个线程依赖的状态无效是不可想象的。这可能导致问题中提到的例外以及其他可能有害的后果。
无论如何,让我们尝试使用代码片段作为起点重现问题,同样在可以利用多次执行的环境中。
static void Main()
{
var table = new DataTable();
table.Columns.Add("Foo");
table.Columns.Add("ID", typeof(int));
for (int i = 0; i < 100; i++)
{
table.Rows.Add(i.ToString(), i);
}
for (int j = 0; j < 100; j++)
{
Enumerable
.Range(0,100)
.AsParallel()
.ForAll(item => ExecuteToTable(table, item));
}
}
static void ExecuteToTable(DataTable table, int item)
{
var view = table.DefaultView;
view.RowFilter = string.Format("Foo = '{0}'", item);
var filteredTable = view.ToTable();
}
这是否会产生异常?跑吧看!它可能需要多次执行,但如果运行代码的机器与我的相似,则不需要很多。 (通过并行查询循环,对我来说真的只需要一次。)
我已在LinqPad中运行此代码,并且已生成“所需”异常。由于这是一个并行查询执行,它将被包装在AggregatedException中,但InnerException将告诉该故事。
KeyNotFoundException:字典中没有给定的键。
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at System.Data.DataView.CopyTo(DataRowView[] array, Int32 index)
at System.Data.DataView.GetEnumerator()
at System.Data.DataView.ToTable(String tableName, Boolean distinct, String[] columnNames)
at System.Data.DataView.ToTable()
所以我们已经复制了它,希望能够理解它。避免它怎么样?有几种可能的解决方案,根据您的使用情况,其中一些可能比其他解决方案更可口,其范围可以是批量重写,也可以是微妙的更改。
您可以
使用table.Copy()
之前复制DataTable 以访问DefaultView。这将为每个请求提供自己的表(在上面的代码段中)。但是,如果表格很大,复制可能会很昂贵。尝试使用Copy()
运行上述复制代码,看看是否避免了异常。
完全避免使用DataView 。 Linq对DataTables也很有用。以下代码段可用于生成过滤的DataTable输出。但是,请注意,如果没有行通过过滤器,CopyToDataTable()
可能会抛出其自己的异常。如果有可能发生这种情况,请在调用最后一部分之前拆分代码并检查结果(使用.Any())。与DataView
相比的另一个缺点是,如果使用可用的DataView
重载,使用ToTable
可以指定要包含在输出表中的列。
var filteredTable
= table.AsEnumerable()
.Where(row => string.Equals(row.Field<string>("Foo"), item.ToString(), StringComparison.InvariantCultureIgnoreCase))
.CopyToDataTable();
当然,您可以进一步探索重新设计代码,将自己的线程安全措施添加到缓存策略等,尽管这些将需要更多时间来实现更改实际表的过滤方式。