Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
    }
}
like image 620
Gone Coding Avatar asked Nov 20 '25 00:11

Gone Coding


1 Answers

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.

like image 108
Stephen Cleary Avatar answered Nov 22 '25 15:11

Stephen Cleary