1

I'd want to know how why creating instances of other classes with current database context instances as a parameter and using that db context causes this exception to be raised

'A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.'

Imma use this sample code to show the problem

public class TestController : Controller
{
    private readonly DbContext dbContext;

    public Controller(DbContext ctx)
    {
        dbContext = ctx;
    }

    public async Task<IActionResult> Test(string id)
    {
        var isValid = new otherClass(dbContext).Validate(id);

        if (!isValid)
        {
            return View("error");
        }

        var user = dbContext.Users.FirstOrDefault(x => x.Id == id);

        user.Age++;

        dbContext.SaveChanges(); // exception is being raised here. It is second .SaveChanges() here  

        return View();
    }
}

public class otherClass
{
    private readonly DbContext dbContext;

    public otherClass(DbContext ctx)
    {
        dbContext = ctx;
    }

    public bool Validate(string id)
    {
        var user = dbContext.Users.FirstOrDefault(x => x.Id == id);

        user.ValidationAttempt = DateTime.Now;

        dbContext.SaveChanges();

        return user.HasConfirmedEmail;
    }
}
Joelty
  • 1,751
  • 5
  • 22
  • 64

1 Answers1

3

Generally in an MVC fashion youre going to want a DbContext on a per request basis but when using threading more control through using blocks can be beneficial, an easy way to set that up would be something along the lines of

public class TestController : Controller
{
    private readonly Func<DbContext> dbContext;

    public Controller(Func<DbContext> ctx)
    {
        dbContext = ctx;
    }

    public async Task<IActionResult> Test(string id)
    {
        using(var cntx = dbContext())
        {
        var isValid = new otherClass(cntx).Validate(id);

        if (!isValid)
        {
            return View("error");
        }

        var user = cntx.Users.FirstOrDefault(x => x.Id == id);

        user.Age++;

        cntx.SaveChanges();  

        return View();
    }
    }
}

that essentially resolves a new DbContext per using block - and since each thread is then handling its own DbContext - shouldnt have any issues

Gibbon
  • 2,633
  • 1
  • 13
  • 19
  • I thoguth that "per request" means "per every instance of controller" – Joelty Nov 05 '18 at 10:59
  • btw. shouldnt be there ``using(var cntx = new dbContext())``? – Joelty Nov 05 '18 at 11:06
  • @Joelty Yeah sorry, slightly poor wording on my part initially - generally a dbContext per instance of the controller is correct - though in your case as you're making use of threading then having a more direct control of context lifetime by using using blocks can be more beneficial to avoid threading issues - and if youre passing in a `Func` to the constructor then it wouldnt need the `new` keyword as the `dbContext()` is a shorthand for `dbContext.Invoke()` which just news up a DbContext using the fun – Gibbon Nov 05 '18 at 11:23
  • sounds cool, but how did you register it? on the other hand: I've been trying to creating new instance of ``dbContext`` basing on ``dbContextOptions`` in constructor of ``otherClass``, but it does not save data to db :/ – Joelty Nov 05 '18 at 11:29
  • @Joelty since it just using `Func` it should just be registered in the same way as a normal DbContext so the `services.AddDbContext ( options => options.UseSqlServer(conn_string) )` you used earlier without `Func` should just work. – Gibbon Nov 05 '18 at 11:35
  • Weird, because he has problems with handling it. ``no service have been registered for type Func[dbContext]`` – Joelty Nov 05 '18 at 12:04
  • @Joelty ahh, if youre just using default Microsoft DI you might need it to be `services.AddTransient>(x => () => x.GetService());` something along those lines to use `Func` correctly. Im too used to using Autofac / other DI's where they essentially take care of it under the hood for Funcs – Gibbon Nov 05 '18 at 12:20