0

I have MVC web app that uses EntityFramework context and it stores it in HttpContext.Current.Items. When HttpContext.Current isn't available then it uses CallContext.SetData to store data in current thread storage. HttpContext is used for web app itself and CallContext is used in unit tests to store the same EF DbContext there.

We are also trying to use async\await as we have library that relays a lot on them, and it works great in web app. But it fails in unit tests as CallContext.SetData isn't restored after thread returns to await block. Here is simplified sample of the issue:

public async Task Test()
{
    ContextUtils.DbContext = new SomeDbContext();
    using (ContextUtils.DbContext){
        await DoSomeActions();
    }
}

public async Task DoSomeActions(){

    var data = await new HttpClient().GetAsync(somePage);

    // on next line code would fail as ContextUtils.DbContext is null
    // as it wasn't synced to new thread that took it
    var dbData = ContextUtils.DbContext.SomeTable.First(...);
}

So in that example ContextUtils.DbContext basically sets HttpContext\CallContext.SetData. And it works fine for web app, and fails in unit test as SetData isn't shared and on ContextUtils.DbContext.SomeTable.First(...); line DbContext is null.

I know that we can use CallContext.LogicalSetData\LogicalGetData and it would be shared withing ExecutionContext, but it requires item to be Serializable and i don't want to mark DbContext with serialization attribute as would try to serialize it.

I also saw Stephen's AsyncEx library (https://github.com/StephenCleary/AsyncEx) that has own SynchronizationContext, but it would require me to update my code and use AsyncContext.Run instead of Task.Run, and i'm trying to avoid code updating just for unit tests.

Is there any way to fix it without changing the code itself just to make it work for unit tests? And where EF DbContext should be stored in unit tests without passing it as parameter and to be able to use async\await?

Thanks

Sergey Litvinov
  • 7,408
  • 5
  • 46
  • 67
  • 1
    Unit-testing is the reason why I tell my co-workers not to use `HttpContext.Current`, I often do a search for it in the code and if someone uses it he'll have to buy me a drink. Storing context on a thread seems even worse. Use proper dependency injection and a library like Autofac would be my advise. – huysentruitw Jul 01 '17 at 20:45
  • Yeah, DI container would help here, but it's a big project that doesn't have DI so adding container there just to fix async\await might be big overkill though i agree DI container is the correct way to go here – Sergey Litvinov Jul 01 '17 at 20:59
  • 1
    Refactoring is always better than adding hacks, even in a big project. But before refactoring, you want to fix the current behavior, if that doesn't work with unit testing then use integration testing and start refactoring + add proper unit tests during the process. – huysentruitw Jul 01 '17 at 21:12

2 Answers2

4

OK, there's a lot of things here.

Personally, I would look askance at the use of CallContext.GetData as a fallback to HttpContext.Current, especially since your code makes use of async. Consider using AsyncLocal<T> instead. However, it's possible that AsyncLocal<T> may also require serialization.

I also saw Stephen's AsyncEx library (https://github.com/StephenCleary/AsyncEx) that has own SynchronizationContext, but it would require me to update my code and use AsyncContext.Run instead of Task.Run, and i'm trying to avoid code updating just for unit tests.

A couple of things here:

  1. You shouldn't be using Task.Run on ASP.NET in the first place.
  2. Using Task.Run will prevent the (non-logical) call context from working, as well as HttpContext.Current. So I assume that your code is not accessing the DbContext from within the Task.Run code.

It sounds like your best option is to use my AsyncContext. This class was originally written for asynchronous unit tests (back before unit test frameworks supported asynchronous unit tests). You shouldn't need to update your code at all; just use it in your unit tests:

public void Test()
{
  AsyncContext.Run(async () =>
  {
    ContextUtils.DbContext = new SomeDbContext();
    using (ContextUtils.DbContext)
    {
      await DoSomeActions();
    }
  });
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Hi @Stephen, thank you very much for your help. `AsyncLocal` was the thing that helped me. I checked source code and `ExecutionContext` has additional field just for `IAsyncLocal` variables and it passes them within context and don't try to serialize. Basically it's `Dictionary` that holds actual value per context and passes in it async calls. Thanks again! – Sergey Litvinov Jul 15 '17 at 13:49
0

Avoid using async void. Make the test method await-able by used Task

[TestMethod]
public async Task Test() {
    ContextUtils.DbContext = new SomeDbContext();
    using (ContextUtils.DbContext) {
        await DoSomeActions();
    }
}

HttpContext is not available during unit test as it is tied to IIS which is not present during unit tests. Avoid tightly coupling your code to HttpContext treat it like a 3rd party resource and abstract it away behind code you can control. It will make testing maintaining and testing your code easier. Consider reviewing your current design.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Hey, yeah, i understand that `HttpContext` isnt available and reasons for that. That's why i mentioned that we use CallContext.SetData for storing data and that it isn't shared in await calls that is actual problem. And as for `void`, it's just exmaple and in real code we don't have it. The real code is just much bigger. The question that i asked is where to store EF DbContext in unit test and be able to use await\async – Sergey Litvinov Jul 01 '17 at 20:00