Sharing DbContext between multiple methods and classes

21 בספטמבר 2014

I've been working with EntityFramework for more than 3 years and I love the technology. For many cases it simplifies the work of accessing the database.

It general, it is recommended to keep EntityFramework DbContext manipulation at the DAL level and not let it propagate to higher layers (BL and Presentation)

So, suppose I am using the repository pattern. The application does not access the DbContext object directly but rather is using a repository object which encapsulates all the data access logic. For example,

public class LoginRepository
{
    public Login GetLoginByID(int loginId)
    {
        using (MyDbContext context = new MyDbContext())
        {
            var login = context.Logins.Where(l => l.ID == loginId).Single();
            return login;
        }
    }

    public void DeleteLogin(int loginId)
    {
        using (MyDbContext context = new MyDbContext())
        {
            var login = GetLoginByID(loginId);
            context.Logins.Remove(login);

            context.SaveChanges();
        }
    }
}

This is quite a straightforward implementation. Let's use it from a Main method

static void Main(string[] args)
{
    try
    {
        LoginRepository logins = new LoginRepository();

        logins.DeleteLogin(1);
    }
    catch (Exception e)
    {
        Console.WriteLine("ERROR: " + e.ToString());
    }
}

Surprisingly, the following error is reported

The object cannot be deleted because it was not found in the ObjectStateManager

Can you notice the reason?

The method DeleteLogin creates its own DbContext and tries to remove a Login object that was returned by the GetLoginByID method. The problem is that the method GetLoginByID creates its own DbContext instance and the Login object returned by it cannot be manipulated by another DbContext.

You can fix that by attaching the Login object to the new context

context.Entry(login).State = System.Data.Entity.EntityState.Deleted;

Although above code works, it does not try to handle the real issue. The methods DeleteLogin and GetLoginByID do not share the DbContext instance. This behavior can effect transaction management too.

Sharing DbContext across different methods or different classes is not an easy task, You can change every method to accept additional parameter of type DbContext. However, this means that the client must create the instance itself or you can create additional layer of methods that create the DbContext instance and pass it to another method with the same name which accept a DbContext object. This requires a lot of coding.

Better solution, is to use the TLS (Thread Local Storage)

Each method that requires a DbContext instance does not create it directly but rather asks it from a another class named DbScope. DbScope should be smart enough to work with the TLS to understand whether a DbContext object was created or not. Using DbScope class, our code changes to

public class LoginRepository
{
    public Login GetLoginByID(int loginId)
    {
        using (var scope = MyDbContext.Requires())
        {
            var login = scope.DbContext.Logins.Where(l => l.ID == loginId).Single();
            return login;
        }
    }

    public void DeleteLogin(int loginId)
    {
        using (var scope = MyDbContext.Requires())
        {
            var login = GetLoginByID(loginId);
            scope.DbContext.Logins.Remove(login);
        }
    }
}

The main difference is that the code does not instantiate a DbContext object directly but rather uses the MyDbContext.Requires method

public class MyDbContext : DbContext
{
    public DbSet<Login> Logins { getset; }

    public static DbScope<MyDbContext> Requires()
    {
        return DbScope<MyDbContext>.Requires(() =>
            {
                return new MyDbContext();
            });
    }
}

As you can see the Requires method is just a thin wrapper around DbScope.Requires

public static DbScope<T> Requires(Func<T> factory)
{
    DbScope<T> scope = Current;
    if (scope == null)
    {
        scope = new DbScope<T>(factory());
    }
    else
    {
        scope.refCount++;
    }

    return scope;
}

The method creates a new DbScope object only if one does not already exist. The Current property is a thin wrapper around TLS manipulation

public static DbScope<T> Current
{
    get
    {
        DbScope<T> scope = (DbScope<T>)Thread.GetData(slot);
        return scope;
    }
}

Attaching the DbScope object to the calling thread is done at the constructor

internal DbScope(T dbContext)
{
    this.refCount = 1;
    this.dbContext = dbContext;
    this.pendingCommands = new List<PendingCommand>();
    this.props = new Dictionary<stringobject>();
    Thread.SetData(slot, this);
}

And the Dispose method clears the current DbScope object only if there is no code that uses it (using reference counting)

public void Dispose()
{
    if (--this.refCount == 0)
    {
        Thread.SetData(slot, null);

        try
        {
            if (Marshal.GetExceptionCode() == 0)
            {
                SaveChanges();
            }
            else
            {
                //
                //  We are in the middle of exception. Do not save changes
                //
            }
        }
        finally
        {
            this.dbContext.Dispose();
            this.dbContext = null;
        }
    }
}

If you read above source code carfully, you probably noticed the SaveChanges invocation. This is a nice feature that can be added to DbScope (not a must). I call it Automatic DbContext Commit. The idea is that the first code which creates the DbScope object is the one that can commit all DbContext changes. However, changes should not be committed in case of exception. The Marshal.GetExceptionCode can detects this. Cool, right?

All source code can be downloaded from here

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*