c# - MSTest期间的终结器中的NullReferenceException

标签 c# .net mstest system.management

(我知道,这是一个荒谬的问题。到目前为止,我试图将这个问题与我的调查分开,所以它稍微容易阅读。)

我正在使用MSTest.exe运行单元测试。有时,我看到此测试错误:

关于单个单元测试方法:“在运行测试时,代理进程已停止。”

在整个测试运行中:

One of the background threads threw exception: 
System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Runtime.InteropServices.Marshal.ReleaseComObject(Object o)
   at System.Management.Instrumentation.MetaDataInfo.Dispose()
   at System.Management.Instrumentation.MetaDataInfo.Finalize()

So, here's what I think I need to do: I need to track down what is causing the error in MetaDataInfo, but I'm drawing a blank. My unit test suite takes over half an hour to run, and the error doesn't happen every time, so it's hard to get it to reproduce.

Has anyone else seen this type of failure in running unit tests? Were you able to track it down to a specific component?

Edit:

The code under test is a mix of C#, C++/CLI, and a little bit of unmanaged C++ code. The unmanaged C++ is used only from the C++/CLI, never directly from the unit tests. The unit tests are all C#.

The code under test will be running in a standalone Windows Service, so there's no complication from ASP.net or anything like that. In the code under test, there's threads starting & stopping, network communication, and file I/O to the local hard drive.


My investigation so far:

I spent some time digging around the multiple versions of the System.Management assembly on my Windows 7 machine, and I found the MetaDataInfo class in System.Management that's in my Windows directory. (The version that's under Program Files\Reference Assemblies is much smaller, and doesn't have the MetaDataInfo class.)

Using Reflector to inspect this assembly, I found what seems to be an obvious bug in MetaDataInfo.Dispose():

// From class System.Management.Instrumentation.MetaDataInfo:
public void Dispose()
{
    if (this.importInterface == null) // <---- Should be "!="
    {
        Marshal.ReleaseComObject(this.importInterface);
    }
    this.importInterface = null;
    GC.SuppressFinalize(this);
}

使用此“if”语句向后,MetaDataInfo将泄漏COM对象(如果存在),否则将抛出NullReferenceException。我已经在Microsoft Connect上报告了此问题:https://connect.microsoft.com/VisualStudio/feedback/details/779328/

使用反射器,我能够找到MetaDataInfo类的所有用法。 (这是一个内部类,因此仅搜索程序集应该是一个完整列表。)只有一个地方被使用:
public static Guid GetMvid(Assembly assembly)
{
    using (MetaDataInfo info = new MetaDataInfo(assembly))
    {
        return info.Mvid;
    }
}

由于已正确处理MetaDataInfo的所有使用,因此正在发生以下情况:
  • 如果MetaDataInfo.importInterface不为null:
  • 静态方法GetMvid返回MetaDataInfo.Mvid
  • using调用MetaDataInfo.Dispose
  • Dispose泄漏COM对象
  • Dispose将importInterface设置为空
  • Dispose调用GC.SuppressFinalize
  • 稍后,当GC收集MetaDataInfo时,将跳过终结器。
  • 如果MetaDataInfo.importInterface为null:
  • 静态方法GetMvid获取一个调用MetaDataInfo.Mvid的NullReferenceException。
  • 在异常传播之前,using调用MetaDataInfo.Dispose
  • Dispose调用Marshal.ReleaseComObject
  • Marshal.ReleaseComObject引发NullReferenceException。
  • 因为引发了异常,所以Dispose不会调用GC.SuppressFinalize
  • 异常传播到GetMvid的调用方。
  • 稍后,当GC收集MetaDataInfo时,它将运行Finalizer
  • 完成调用处置
  • Dispose调用Marshal.ReleaseComObject
  • Marshal.ReleaseComObject引发NullReferenceException,该异常一直传播到GC,应用程序终止。

  • 对于它的值(value),这是MetaDataInfo的其余相关代码:
    public MetaDataInfo(string assemblyName)
    {
        Guid riid = new Guid(((GuidAttribute) Attribute.GetCustomAttribute(typeof(IMetaDataImportInternalOnly), typeof(GuidAttribute), false)).Value);
        // The above line retrieves this Guid: "7DAC8207-D3AE-4c75-9B67-92801A497D44"
        IMetaDataDispenser o = (IMetaDataDispenser) new CorMetaDataDispenser();
        this.importInterface = (IMetaDataImportInternalOnly) o.OpenScope(assemblyName, 0, ref riid);
        Marshal.ReleaseComObject(o);
    }
    
    private void InitNameAndMvid()
    {
        if (this.name == null)
        {
            uint num;
            StringBuilder szName = new StringBuilder {
                Capacity = 0
            };
            this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
            szName.Capacity = (int) num;
            this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
            this.name = szName.ToString();
        }
    }
    
    public Guid Mvid
    {
        get
        {
            this.InitNameAndMvid();
            return this.mvid;
        }
    }
    

    编辑2:

    我能够重现Microsoft的MetaDataInfo类中的错误。但是,我的复制与我在这里看到的问题略有不同。
  • 复制:我尝试在非托管程序集的文件上创建MetaDataInfo对象。初始化importInterface之前,这会从构造函数中引发异常。
  • 我与MSTest有关的问题:MetaDataInfo是在某些托管程序集上构造的,并且使importInterface为null,或者在初始化importInterface之前退出了构造函数。
  • 我知道MetaDataInfo是在托管程序集上创建的,因为MetaDataInfo是内部类,并且唯一调用它的API是通过传递Assembly.Location的结果来实现的。

  • 但是,在Visual Studio中重新创建该问题意味着它将为我将源下载到MetaDataInfo。这是实际的代码,带有原始开发人员的注释。
    public void Dispose()
    { 
        // We implement IDisposable on this class because the IMetaDataImport
        // can be an expensive object to keep in memory. 
        if(importInterface == null) 
            Marshal.ReleaseComObject(importInterface);
        importInterface = null; 
        GC.SuppressFinalize(this);
    }
    
    ~MetaDataInfo() 
    {
        Dispose(); 
    } 
    

    原始代码确认了在反射器中看到的内容:if语句是向后的,它们不应从Finalizer访问托管对象。

    我之前说过,因为它从未调用ReleaseComObject,所以它泄漏了COM对象。我阅读了有关.Net中COM对象使用的更多信息,如果我正确理解的话,那是不正确的:调用Dispose()时不会释放COM对象,但是当垃圾收集器处理完该对象时就会释放COM对象。收集运行时可调用包装程序,它是一个托管对象。尽管它是非托管COM对象的包装,但RCW仍然是托管对象,关于“不要从终结器访问托管对象”的规则仍然适用。

    最佳答案

    尝试将以下代码添加到您的类定义中:

    bool _disposing = false  // class property
    
    public void Dispose()
    {
        if( !disposing ) 
            Marshal.ReleaseComObject(importInterface);
        importInterface = null; 
        GC.SuppressFinalize(this);
    
        disposing = true;
    }
    

    关于c# - MSTest期间的终结器中的NullReferenceException,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14903295/

    相关文章:

    c# - 以编程方式在 Excel 中嵌入对象

    javascript - Json 到 DateTime - 更改格式

    c# - 如何将一个 FlowDocument 中的内联内容插入到另一个 FlowDocument 中?

    javascript - 在页面加载时运行 javascript 函数

    c# - 如何获取CodeAttribute中的属性值

    visual-studio-2008 - 更改默认的MsTest单元测试向导模板

    mstest - 在 MSTest 的测试 View 中,如何获取所有没有类别的测试的列表?

    c# - CopyListBoxItem 从一个 ListBox 到另一个

    c# - 在 WPF 中打印带有动态数据的流文档

    visual-studio - 如何检查项目是否为测试项目? (NUnit、MSTest、xUnit)