从控制器的构造函数运行异步方法出现问题

7

我正在处理一个项目,想要使用访问令牌/刷新令牌来保持用户的登录状态。我将这些值存储在cookie中,每当用户访问网站时,无论他使用哪个页面访问该网站,我都希望自动登录他。为此,我创建了一个BaseController,所有其他控制器都继承自该控制器。BaseController看起来像这样:

public abstract class BaseController : Controller
{
    public BaseController()
    {
        LoginModel.SetUserFromAuthenticationCookie();
    }
}

每次执行操作之前,都会执行此构造函数,这正是我想要的。问题在于SetUserFromAuthenticationCookie()是一个异步方法,因为它必须调用其他异步方法。它看起来像这样:

public async static Task SetUserFromAuthenticationCookie()
    {
        // Check if the authentication cookie is set and the User is null
        if (AuthenticationRepository != null && User == null)
        {
            Api api = new Api();

            // If a new authentication cookie was successfully created
            if (await AuthenticationRepository.CreateNewAuthenticationCookieAsync())
            {
                var response = await api.Request(HttpMethod.Get, "api/user/mycredentials");

                if(response.IsSuccessStatusCode)
                {
                    User = api.serializer.Deserialize<UserViewModel>(await response.Content.ReadAsStringAsync());
                }
            }
        }
    }

问题在于执行顺序不如预期,因此用户无法登录。我尝试使用异步方法的.Result,但结果导致死锁。除此之外,我阅读了许多关于该问题的SO线程,并最终找到一个成功使登录工作的线程:如何以同步方式运行异步Task<T>方法?。虽然这有点繁琐,但它可以通过这个辅助程序实现:
public static class AsyncHelpers
{
    /// <summary>
    /// Execute's an async Task<T> method which has a void return value synchronously
    /// </summary>
    /// <param name="task">Task<T> method to execute</param>
    public static void RunSync(Func<Task> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        synch.Post(async _ =>
        {
            try
            {
                await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();

        SynchronizationContext.SetSynchronizationContext(oldContext);
    }

    /// <summary>
    /// Execute's an async Task<T> method which has a T return type synchronously
    /// </summary>
    /// <typeparam name="T">Return Type</typeparam>
    /// <param name="task">Task<T> method to execute</param>
    /// <returns></returns>
    public static T RunSync<T>(Func<Task<T>> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        T ret = default(T);
        synch.Post(async _ =>
        {
            try
            {
                ret = await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();
        SynchronizationContext.SetSynchronizationContext(oldContext);
        return ret;
    }

    private class ExclusiveSynchronizationContext : SynchronizationContext
    {
        private bool done;
        public Exception InnerException { get; set; }
        readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
        readonly Queue<Tuple<SendOrPostCallback, object>> items =
            new Queue<Tuple<SendOrPostCallback, object>>();

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotSupportedException("We cannot send to our same thread");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            lock (items)
            {
                items.Enqueue(Tuple.Create(d, state));
            }
            workItemsWaiting.Set();
        }

        public void EndMessageLoop()
        {
            Post(_ => done = true, null);
        }

        public void BeginMessageLoop()
        {
            while (!done)
            {
                Tuple<SendOrPostCallback, object> task = null;
                lock (items)
                {
                    if (items.Count > 0)
                    {
                        task = items.Dequeue();
                    }
                }
                if (task != null)
                {
                    task.Item1(task.Item2);
                    if (InnerException != null) // the method threw an exeption
                    {
                        throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
                    }
                }
                else
                {
                    workItemsWaiting.WaitOne();
                }
            }
        }

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }
    }

如果我将BaseController构造函数的内容更改为:

如果我将BaseController的构造函数内容更改为:

AsyncHelpers.RunSync(() => LoginModel.SetUserFromAuthenticationCookie());

该功能按预期工作。

但我想知道是否有更好的方法来完成此操作。也许我应该将对SetUserFromAuthenticationCookie()的调用移动到其他位置,但目前我不知道应该在哪里。


你不会使用 await LoginModel.SetUserFromAuthenticationCookie(); 吗? - JamieD77
LoginModel.SetUserFromAuthenticationCookie().RunSynchronously(); 转换为中文: - JamieD77
我无法回答关于异步操作的问题...但是如果你想让这段代码在每个操作之前执行,你可以考虑创建一个全局操作过滤器。 - Peter
@Peter 我之前尝试过,但是仍然出现了同样的问题。MVC操作过滤器不支持异步... - user1796440
1
是的,您仍然需要同步运行它。我只是说,与其将其放在构造函数中,动作过滤器可能是更好的选择。 - Peter
显示剩余3条评论
1个回答

13

我不知道我可以在Task的Run函数中使用async/await。谢谢你提醒我!现在我遇到的问题是,我有些部分依赖于HttpContext.Current.xxx,在新创建的线程中HttpContext.Current(显然)为空。我已经计划重构我的代码,并将开始处理这个问题。在完成这个任务并确定你的建议是否解决了我的问题之后,我会回到这个问题。非常感谢你迄今为止的帮助! - user1796440

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接