Should I Treat Entity Framework as an Unmanaged Resource?

大憨熊 提交于 2020-01-11 08:29:05

问题


I am working with a class that uses a reference to EF in its constructor.

I have implemented IDisposable, but I'm not sure if I need a destructor because I'm not certain I can classify EF as an unmanaged resource.

If EF is a managed resource, then I don't need a destructor, so I think this is a proper example:

public ExampleClass : IDisposable
{
    public ExampleClass(string connectionStringName, ILogger log)
    {
        //...
        Db = new Entities(connectionStringName);
    }

    private bool _isDisposed;

    public void Dispose()
    {
        if (_isDisposed) return;

        Db.Dispose();

        _isDisposed= true;
    }
}

If EF is unmanaged, then I'll go with this:

public ExampleClass : IDisposable
{
    public ExampleClass(string connectionStringName, ILogger log)
    {
        //...
        Db = new Entities(connectionStringName);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    ~ExampleClass()
    {
        Dispose(false);
    }

    private bool _isDisposed;

    protected virtual void Dispose(bool disposing)
    {
        if (_isDisposed) return;

        // Dispose of managed resources
        if (disposing)
        {
            // Dispose of managed resources; assumption here that EF is unmanaged.
        }
        // Dispose of unmanaged resources
        Db.Dispose();

        _isDisposed = true;
        //freed, so no destructor necessary.
        GC.SuppressFinalize(this);

    }
}

Which one is it?


回答1:


You would never want to use a finalizer (destructor) in this case.

Whether DbContext contains unmanaged resources or not, and even whether it responsibly frees those unmanaged resources or not, is not relevant to whether you can try to invoke DbContext.Dispose() from a finalizer.

The fact is that, any time you have a managed object (which an instance of DbContext is), it is never safe to attempt to invoke any method on that instance. The reason is that, by the time the finalizer is invoked, the DbContext object may have already been GC-collected and no longer exist. If that were to happen, you would get a NullReferenceException when attempting to call Db.Dispose(). Or, if you're lucky, and Db is still "alive", the exception can also be thrown from within the DbContext.Dispose() method if it has dependencies on other objects that have since been finalized and collected.

As this "Dispose Pattern" MSDN article says:

X DO NOT access any finalizable objects in the finalizer code path, because there is significant risk that they will have already been finalized.

For example, a finalizable object A that has a reference to another finalizable object B cannot reliably use B in A’s finalizer, or vice versa. Finalizers are called in a random order (short of a weak ordering guarantee for critical finalization).

Also, note the following from Eric Lippert's When everything you know is wrong, part two:

Myth: Finalizers run in a predictable order

Suppose we have a tree of objects, all finalizable, and all on the finalizer queue. There is no requirement whatsoever that the tree be finalized from the root to the leaves, from the leaves to the root, or any other order.

Myth: An object being finalized can safely access another object.

This myth follows directly from the previous. If you have a tree of objects and you are finalizing the root, then the children are still alive — because the root is alive, because it is on the finalization queue, and so the children have a living reference — but the children may have already been finalized, and are in no particularly good state to have their methods or data accessed.


Something else to consider: what are you trying to dispose? Is your concern making sure that database connections are closed in a timely fashion? If so, then you'll be interested in what the EF documentation has to say about this:

By default, the context manages connections to the database. The context opens and closes connections as needed. For example, the context opens a connection to execute a query, and then closes the connection when all the result sets have been processed.

What this means is that, by default, connections don't need DbContext.Dispose() to be called to be closed in a timely fashion. They are opened and closed (from a connection pool) as queries are executed. So, though it's still a very good idea to make sure you always call DbContext.Dispose() explicitly, it's useful to know that, if you don't do it or forget for some reason, by default, this is not causing some kind of connection leak.


And finally, one last thing you may want to keep in mind, is that with the code you posted that doesn't have the finalizer, because you instantiate the DbContext inside the constructor of another class, it is actually possible that the DbContext.Dispose() method won't always get called. It's good to be aware of this special case so you are not caught with your pants down.

For instance, suppose I adjust your code ever so slightly to allow for an exception to be thrown after the line in the constructor that instantiates the DbContext:

public ExampleClass : IDisposable
{
    public ExampleClass(string connectionStringName, ILogger log)
    {
        //...
        Db = new Entities(connectionStringName);

        // let's pretend I have some code that can throw an exception here.
        throw new Exception("something went wrong AFTER constructing Db");
    }

    private bool _isDisposed;

    public void Dispose()
    {
        if (_isDisposed) return;

        Db.Dispose();

        _isDisposed= true;
    }
}

And let's say your class is used like this:

using (var example = new ExampleClass("connString", log))
{
    // ...
}

Even though this appears to be a perfectly safe and clean design, because an exception is thrown inside the constructor of ExampleClass after a new instance of DbContext has already been created, ExampleClass.Dispose() is never invoked, and by extension, DbContext.Dispose() is never invoked either on the newly created instance.

You can read more about this unfortunate situation here.

To ensure that the DbContext's Dispose() method is always invoked, no matter what happens inside the ExampleClass constructor, you would have to modify the ExampleClass class to something like this:

public ExampleClass : IDisposable
{
    public ExampleClass(string connectionStringName, ILogger log)
    {
        bool ok = false;
        try 
        {
            //...
            Db = new Entities(connectionStringName);

            // let's pretend I have some code that can throw an exception here.
            throw new Exception("something went wrong AFTER constructing Db");

            ok = true;
        }
        finally
        {
            if (!ok)
            {
                if (Db != null)
                {
                    Db.Dispose();
                }
            }
        }
    }

    private bool _isDisposed;

    public void Dispose()
    {
        if (_isDisposed) return;

        Db.Dispose();

        _isDisposed= true;
    }
}

But the above is really only a concern if the constructor is doing more than just creating an instance of a DbContext.




回答2:


C# provides garbage collection and thus does not need an explicit destructor. If you do control an unmanaged resource, however, you will need to explicitly free that resource when you are done with it. Implicit control over this resource is provided with a Finalize( ) method (called a finalizer), which will be called by the garbage collector when your object is destroyed.

https://www.oreilly.com/library/view/programming-c/0596001177/ch04s04.html



来源:https://stackoverflow.com/questions/31833599/should-i-treat-entity-framework-as-an-unmanaged-resource

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!