BenjiFB
BenjiFB

Reputation: 4721

Asp.net web api + entity framework: multiple requests cause data conflict

I'm developing an app with VS2013, using EF6.02, and Web API 2. I'm using the ASP.NET SPA template, and creating a RESTful api against an entity framework data source backed by a sql server. (In development, this resides on the SQL Server local instance.)

I've got two API methods so far (one that just reads data, one that writes data), and I'm testing them by calling them in the javascript. When I only call a single method in my script, either one works perfectly. But if I call both in script (without waiting for either's callback to fire), I get bad results and different exceptions in the debugger. Some exceptions state that the save can't be completed because there are pending transactions. Another exception stated something about a conflict with other threads. And sometimes, the read operation fails with a null pointer exception when trying to read a result set.

"New transaction is not allowed because there are other threads running in the session."

This makes me question if I'm correctly getting a new DBContext per request. My code for this looks like:

    static Startup()
    {
        context = new Data.SqlServer.AppDbContext();
  ...
    }

and then whenever instantiating a unit of work, I access Startup.context.

I've tried to implement the unit of work pattern, and each request shares a single UOW object which has a single DBContext object.

My question: Do I have additional responsibility to ensure that web requests "play nicely" with eachother? I hope that this is a problem that others have already dealt with. Perhaps the errors that I'm seeing are legitimate in the sense that if one user's data is being touched, it is temporarily in an invalid state and if other requests come in at that exact moment, they indeed will fail (and I should code anticipating these failures). I guess that even if each request has its own DBContext, they still share the same underlying SQL data source so perhaps that's causing issues.

I can try to put together a testcase, but I get differing behavior depending on where I put breakpoints and how long I spend on them, reaffirming to me that this is timing related.

Thanks for any help or suggestions... -Ben

Upvotes: 3

Views: 2781

Answers (1)

Tommy
Tommy

Reputation: 39807

Your problem is where you are setting your context. The Startup method is for when the entire application starts, thus any request made will all use the same context. This is not a per request setup, but rather a per application setup. As to why you are getting the errors, EntityFramework is NOT thread-safe. Since IIS spawns many threads to handle concurrent request, your single context is being used across multiple threads.

As for a solution, you can look into

-Dependency Injection frameworks (such as Ninject or Unity)

-place a using statement in your UnitOfWork classes

using(var context = new Data.SqlServer.AppDbContext()){//do stuff}

-Or, I have seen instances of people creating a class that gets the context for that request and stores it in the HttpContext.Cache[] element (using a unique name so you can retrieve it in another class easily), making it so that you will reuse the same context for the same request. Something like this:

public AppDbContext GetDbContext()
    {
        var httpContext = HttpContext.Current;
        if (httpContext == null) return new AppDbContext();
        const string contextTypeKey = "AppDbContext";
        if (httpContext.Items[contextTypeKey] == null)
        {
            httpContext.Items.Add(contextTypeKey, new AppDbContext());
        }
        return httpContext.Items[contextTypeKey] as AppDbContext;
    }

To use the above method, make a simple call var context = GetDbContext();

Note

We have all of the above methods, but this is specifically to the third method. It seems to work well with two caveats. First, do not use this in a using statement as it will not be available to any other classes during the scope of the request (you dispose it). And secondly, ensure that you have a call on Application_EndRequest that does actually dispose of it. We saw these little buggers hanging around after the request ended in memory causing a huge spike in memory usage.

Upvotes: 2

Related Questions