2

我正在研究一个遗留的 ASP.NET 代码库,该代码库将一些信息存储在DataTable对象内部的缓存中(通过企业库)。它是一个多用户的 Intranet 环境,在 .NET 4.0 中运行。生产中存在一个问题,该问题指向先前的“已修复”问题作为其可能的根本原因:KeyNotFoundException在调用DataView.ToTable(). 此特定代码是在从应用程序中的大多数页面加载页面期间可能发生的验证的一部分。

var table = GetSomeDataTableFromCache();
var view = table.DefaultView;
view.RowFilter = "Foo = 'Bar'";
var filteredTable = view.ToTable();

这是代码的简化。发生的事情是,在过去的几天里这段代码显然抛出了上述异常。以前的开发人员通过捕获并吞下异常并返回 null 来“修复”它。这种行为是其他已曝光问题的罪魁祸首。

我试图重现原始异常但没有取得多大成功。我觉得如果我能重现它,我就可以理解它,并尝试形成一个比忽略它更好的原始问题的解决方案,从而也避免忽略它造成的混乱。

我注意到谷歌搜索DataView.ToTable() + KeyNotFoundException生成的结果表明验证列名存在。一个示例位于此处:

http://forums.asp.net/t/1695676.aspx/1

但是,我观察到,将 设置RowFilter为无效的列名时,EvaluateException会在该行上发生 an 。我还尝试通过使用空表以及使用产生空结果的过滤器来解决错误。每种情况都证明是非例外的。

那么KeyNotFoundException,如果它不是建议的无效列名,那么会在哪里发生呢?一旦我们知道它是如何发生的,我们如何才能避免它呢?

4

1 回答 1

4

请注意,ASP.NET 本质上是一个多线程系统。DataView被记录为对于读取是线程安全的,但对于写入不是。考虑这一点很重要。

DataTable在呈现的代码片段中,从缓存中拉出一个实例并访问它DefaultView以进行过滤和写入一个新的实例DataTable可能会出现一个证明不安全的条件,这不是因为过滤器中的列,而是因为多个可以同时执行相同代码的线程。

作为实现细节,DataView利用了几位内部状态。在ToTable()执行过程中,涉及到多个非本地状态。特别是,有一个字典字段作为键在 DataRow 上,还有一个 DataRow 字段用作键!该字典被清除,添加到该行,该行被引用行的值覆盖,然后设置为空,这都是该过程的一部分。当多个线程同时执行此操作时,一个线程正在覆盖并使另一个线程所依赖的状态无效并非不可想象。这可能导致问题中提到的异常以及其他潜在的有害后果。

无论如何,让我们尝试使用代码片段作为起点来重现该问题,同样是在可以利用多次执行的环境中。

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()

所以我们已经复制了它,并希望能理解它。怎么避免呢?有几种可能的解决方案,根据您的用例,有些解决方案比其他解决方案更可口,范围可以从大规模重写到细微的更改。

你可以

在访问 DefaultViewtable.Copy() 之前使用复制 DataTable 。这将为每个请求提供自己的表(在上面的代码段中)。但是,如果表很大,复制可能会很昂贵。尝试使用运行上述重现代码Copy()并查看是否避免了异常。

完全避免使用 DataView。Linq 对于 DataTables 也很有用。下面的代码片段可用于生成过滤的 DataTable 输出。但是,还要注意,如果没有行通过过滤器,它CopyToDataTable()可能会抛出自己的异常。如果有可能发生这种情况,请在调用最后一部分之前拆分代码并检查结果(使用 .Any())。与 the 相比的另一个缺点是,如果您使用可用的重载DataView,则使用 aDataView允许您指定希望包含在输出表中的列。ToTable

var filteredTable
        = table.AsEnumerable()
               .Where(row => string.Equals(row.Field<string>("Foo"), item.ToString(), StringComparison.InvariantCultureIgnoreCase))
               .CopyToDataTable();

当然,您可以进一步探索重新设计代码,将您自己的线程安全措施添加到缓存策略等,尽管这些将花费更多时间来实现更改实际表的过滤方式。

于 2013-04-04T03:17:23.017 回答