Reputation: 93571
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:
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
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
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:
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.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
.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
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