iCollect.it Ltd
iCollect.it Ltd

Reputation: 93571

Null exception running async method synchronously with MS AsyncHelper

As you cannot run async methods from child window (@Html.Action) calls, I have been searching for the simplest way to run async tasks from a non-async method. This will allow my MainMenu controller Menu action to still work when injected like this (rather than have to move to a VM or Ajax solution):

<div class="row">
    @Html.Action("MainMenu", "Menu")
</div>

I tried this promising approach using a copy of the code MS use themselves: How to call asynchronous method from synchronous method in C#?

AsyncHelper code:

public static class AsyncHelper
{
    private static readonly TaskFactory _myTaskFactory = new
          TaskFactory(CancellationToken.None,
                      TaskCreationOptions.None,
                      TaskContinuationOptions.None,
                      TaskScheduler.Default);

    public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        return AsyncHelper._myTaskFactory
          .StartNew<Task<TResult>>(func)
          .Unwrap<TResult>()
          .GetAwaiter()
          .GetResult();
    }

    public static void RunSync(Func<Task> func)
    {
        AsyncHelper._myTaskFactory
          .StartNew<Task>(func)
          .Unwrap()
          .GetAwaiter()
          .GetResult();
    }
}

and I am consuming it like this:

    public async Task<ActionResult> MainMenu()
    {
        if (_currentCandidate == null)
        {
            throw new ArgumentNullException("_currentCandidate");
        }
        var candidateId = AsyncHelper.RunSync<int>(() => _currentCandidate.CandidateIdAsync());

        [snip]
    }

Which calls this async method:

    public async Task<int> CandidateIdAsync()
    {
        var applicationUser = await this.ApplicationUserAsync();
        if (applicationUser != null)
        {
            return applicationUser.CandidateId.GetValueOrDefault();
        }
        return 0;
    }

Only when I run this I get the following error:

enter image description here

What am I missing here? The code looks like it should work, but I am not familiar enough with it yet to figure it out.

Update:

For reference the MainMenu controller class looks like this:

public class MenuController : Controller
{
    readonly ICurrentCandidate _currentCandidate;

    public MenuController(ICurrentCandidate currentCandidate)
    {
        _currentCandidate = currentCandidate;
    }

    // GET: MainMenu
    public ActionResult MainMenu()
    {
        if (_currentCandidate == null)
        {
            throw new ArgumentNullException("_currentCandidate");
        }
        var candidateId = AsyncHelper.RunSync<int>(() => _currentCandidate.CandidateIdAsync());

        [snip]            
        return View(vm);
    }
}

Another update:

The failure appears to be inside the related IF code as a simplified CandidateIdAsnyc works:

// This works
public async Task<int> CandidateIdAsync()
{
    return 0;
}

Here is the rest of that code:

public class CurrentCandidate : ICurrentCandidate
{
    private readonly ApplicationDbContext _applicationDbContext;
    private readonly IApplicationUserManager _userManager;
    private readonly ICandidateStore _candidateStore;

    public CurrentCandidate(ApplicationDbContext applicationDbContext, ICandidateStore candidateStore, IApplicationUserManager userManager)
    {
        this._candidateStore = candidateStore;
        this._applicationDbContext = applicationDbContext;
        this._userManager = userManager;    // new ApplicationUserManager(new UserStore<ApplicationUser>(this._applicationDbContext));
    }

    public async Task<ApplicationUser> ApplicationUserAsync()
    {
        var applicationUser = await this._userManager.FindByIdAsync(HttpContext.Current.User.Identity.GetUserId());
        return applicationUser;
    }

    public bool IsAuthenticated()
    {
        return HttpContext.Current.User.Identity.IsAuthenticated;
    }

    public async Task<int> CandidateIdAsync()
    {
        var applicationUser = await this.ApplicationUserAsync();
        if (applicationUser != null)
        {
            return applicationUser.CandidateId.GetValueOrDefault();
        }
        return 0;
    }
}

Upvotes: 3

Views: 3425

Answers (3)

iCollect.it Ltd
iCollect.it Ltd

Reputation: 93571

Thanks for the advice everyone.

None of the "workarounds" worked, and we needed to keep existing async code, so I decided not to fight against async.

I have avoided the problem by not using @Html.Action at all.

Instead I use the menu as a partial view using

@Html.Partial("MainMenu", @ViewBag.MainMenuViewModel);

and setting that model in our base controller.

Upvotes: 1

Stephen Cleary
Stephen Cleary

Reputation: 456507

I have been searching for the simplest way to run async tasks from a non-async method.

This has been discussed many times, and there is no solution that works in every scenario. The internal AsyncHelper type is used only in very specific situations where the ASP.NET team knows it's safe; it's not a general-purpose solution.

Generally, the approaches are:

  1. Block (using Result or GetAwaiter().GetResult()). This approach can cause deadlocks (as I describe on my blog) unless you consistently use ConfigureAwait(false) - and all of the code you call also consistently uses ConfigureAwait(false). However, note that your code can't use ConfigureAwait(false) unless it doesn't actually need the ASP.NET context.
  2. Nested message loop. This is possible using something like AsyncContext from my AsyncEx library. However, there are a lot of ASP.NET APIs that implicitly assume the current SynchronizationContext is AspNetSynchronizationContext, which is not the case within AsyncContext.
  3. Separate thread with blocking (using Task.Run(...).GetAwaiter().GetResult()). This approach avoids the deadlocks you can see with just blocking, but it does execute the code outside of an ASP.NET context. This approach also negatively impacts your scalability (which is the whole point of using async on ASP.NET in the first place).

In other words, these are just hacks.

ASP.NET vNext has the concept of "view components" which may be async, so this will be a natural solution in the future. For today's code, IMO the best solution is to make the methods synchronous; that's better than implementing a hack.

Upvotes: 4

Alexei Levenkov
Alexei Levenkov

Reputation: 100547

Quite standard way of running async methods synchronously is

var r = Task.Run( () => MyAsynchMethod(args)).Result;

Or

var task = MyAsynchMethod(args);
task.ConfigureAwait(false);
var r = task.Result;

Both will not cause deadlock as they will run at least asynchronous return part of the method without ASP.Net Synchronization context.

Now most ASP.Net-related methods (like rendering or identity in your case) expect HttpContext.Current to be correctly set to "current" context - which is explicitly not the case when code ends up running on thread that did not "entered" ASP.Net synchronization context (you also loose current culture, but at least it will not cause NRE).

Dirty workaround - set context manually, but you run into danger of accessing same context in parallel from multiple threads - if used carefully it may be ok (i.e. if "main" request thread is waiting on .Result other thread can use it):

 var context = HttpContext.Current;  
 var r = Task.Run( () => 
    {
       HttpContext.Current = context;
       return MyAsynchMethod(args);
    }
).Result;

Note that you'd better restore context and set/restore both CurrentCulture/CurrentUICulture too.

Upvotes: -1

Related Questions