DOTang DOTang - 2 months ago 14
C# Question

How to serve up a properly scoped DbContext in an NTier Api that is application-type agnostic?

I am writing a class library using EF Code First that I expect to be used in all types of applications including web, console, and desktop. Currently my API, which exists only as a web API, uses a standard factory pattern to return a DbContext instance scoped to HttpContext.

The method call looks like this:

EntityDbContextFactory<MyDbContext>.GetInstance();


And the implementation:

public class EntityDbContextFactory<TContext>
where TContext : class, IDisposable, new()
{
private static TContext _dbContext;

public static TContext GetInstance()
{
TContext context;

if (HttpContext.Current != null)
{
var objectContextKey = HttpContext.Current.GetHashCode().ToString("x") +
typeof(TContext).GetHashCode().ToString(CultureInfo.InvariantCulture);

if (!HttpContext.Current.Items.Contains(objectContextKey))
{
context = new TContext();
HttpContext.Current.Items.Add(objectContextKey, context);
}
else
{
context = HttpContext.Current.Items[objectContextKey] as TContext;
}
}
else
{
if (_dbContext == null)
{
_dbContext = new TContext();
context = _dbContext;
}
else
{
context = _dbContext;
}
}


return context;
}
}


Very standard stuff, I've seen this in many places online. I need to supply this kind of scoping manually because I can't guarantee programmers using my API are using a DI container, and even if I could, I don't want my repository tracking state (by injecting the DbContext into the repository constructor as commonly seen), nor do I want people calling my services to have any care about my data access method.

Now the problem becomes, how do I expand this out to encompass console and desktop apps? If I use the above in those apps, I have to reference System.Web, which doesn't seem appropriate at all.

Standard singleton patterns could work for desktop and console, but then if either app is open a long time, the entities cached in the context could very well become stale. So I certainly don't want to do full singleton for lifetime of apps approach, but then how to you scope them? I would like to be able to still be efficient with my DbContexts creation (and service and repository creation) and not create new ones every single time if possible. And not rely on external libraries.

Answer

Well it seems silly, but I am almost already doing that. As mentioned by the link in the comments, you don't want to keep a context alive at all in a console or desktop app because those apps are stateful and the context can become stale very quickly. So for those instances, you should be using a new instance every time it's called for.

However, in a web application which has no state, it is appropriate and common practice to scope the DbContext to the HttpContext (Per Web Request). Each request may contain multiple service calls and it makes sense to only use one dbcontext instance per request. Almost think of it like a transaction. There are numerous examples from Microsoft themselves showing this.

However, my approach has an added benefit. Most examples online you see of NTier using this approach store the dbcontext as a member of the repository class. This makes your NTier API stateful and then as required now (since the dbcontext is not threadsafe) you must scope your repository and service classes to one instance per request. But with my approach, you can scope your service and repository classes as singletons, which is more efficient.

Common scenario seen online:

public class UserRepository
{
    private MyDbContext _myDbContext;

    public UserRepository(MyDbContext myDbContext)
    {
        _myDbContext = myDbContext;
    }
}

DbContext is not thread safe, therefore every layer in your ntier app (commonly dboncontext -> repository -> service) must be scoped to per http request in your DI container.

My approach:

public class UserRepository<TbContext>
{
    private DbContext Context
    {
        get { return EntityContextFactory<TDbContext>.Current(); }
    }
}

Using the above code in the question for the factory class. Now my services and repositories can be singletons and regardless of what kind of app I am using my API in, it will handle the dbcontext lifetime as appropriate. The only change that is needed, is if HttpContext is null, return a new instance and don't store it as a private member:

public class EntityContextFactory<TContext>
    where TContext: class, IDisposable, new()
{
    private static TContext _dbContext;

    public static TContext Current()
    {
        TContext context = null;

        if (HttpContext.Current != null)
        {
            string objectContextKey = HttpContext.Current.GetHashCode().ToString("x") +
                                      typeof (TContext).GetHashCode().ToString(CultureInfo.InvariantCulture);

            if (HttpContext.Current.Items.Contains(objectContextKey) == false)
            {
                context = new TContext();
                HttpContext.Current.Items.Add(objectContextKey, context);
            }
            else
            {
                context = HttpContext.Current.Items[objectContextKey] as TContext;
            }
        }
        else
        {
                context = new TContext();
        }


        return context;
    }


}