7

I am working on a project where I want to keep users logged in using access tokens/refresh tokens. I store these values in a cookie and whenever a user visits the site, I want to automatically log him in regardless of the page that he uses to access the site. To do this, I created a BaseController, that all other controllers inherit from. The BaseController looks like this:

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

This constructor gets executed every time before an action is executed and is therefore exactly what I want. The problem is that SetUserFromAuthenticationCookie() is an async method, because it has to do calls to other async methods. It looks like this:

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());
                }
            }
        }
    }

The problem is that the execution order is not as I anticipated and because of that the user does not get logged in. I tried to work with .Result for the async methods, but that resulted in a deadlock. Besides that I read many threads on SO concerning the issue and eventually also found one that managed to get the login to work: How would I run an async Task<T> method synchronously?. It is somewhat hacky though and works with this helper:

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;
        }
    }

If I then change the content of the BaseController constructor to:

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

the functionality works as anticipated.

I would like to know though if you have any suggestions on how to do this in a nicer manner. Perhaps I should move the call to the SetUserFromAuthenticationCookie() to another location, but at this time I do not know where that would be.

user247702
  • 23,641
  • 15
  • 110
  • 157
user1796440
  • 366
  • 3
  • 11
  • you wouldnt use `await LoginModel.SetUserFromAuthenticationCookie();`? – JamieD77 Aug 27 '15 at 14:58
  • or `LoginModel.SetUserFromAuthenticationCookie().RunSynchronously();` – JamieD77 Aug 27 '15 at 15:12
  • I can't answer about the async stuff ... but if you want this code to execute before every action, you might consider creating a global action filter. – Peter Aug 27 '15 at 16:17
  • @JamieD77 I can't await it from the constructor, that's the problem... and RunSynchronously() does not do the job. – user1796440 Aug 27 '15 at 17:19
  • @Peter I tried that before, but the same problem occurs. MVC action filters aren't async... – user1796440 Aug 27 '15 at 17:21
  • you can create a private function or void to call from your constructor that can await the call to SetUserFromAuthenticationCookie – JamieD77 Aug 27 '15 at 17:23
  • 1
    Yes, you'd still have to run it synchronously. I'm just saying that an action filter might be a better place to put it than the constructor. – Peter Aug 27 '15 at 17:25
  • @JamieD77 But then I wouldn't be able to await that function from the constructor, so what's the difference? – user1796440 Aug 27 '15 at 19:13

1 Answers1

13

I found this solution on another stack. Synchronously waiting for an async operation, and why does Wait() freeze the program here

Your constructor would need to look like this.

public BaseController()
{
    var task = Task.Run(async () => { await LoginModel.SetUserFromAuthenticationCookie(); });
    task.Wait();
}
Community
  • 1
  • 1
JamieD77
  • 13,796
  • 1
  • 17
  • 27
  • I didn't know I could use async/await in Task's Run function. Thanks for the heads up! The problem that I run into now is that I for some parts rely on HttpContext.Current.xxx and in the newly created thread HttpContext.Current (obviously) is null. I was already planning on refactoring those parts of my code and will now start working on this. After I'm done with that and know whether or not your suggestion resolved my issue, I will come back to this question. Thanks for your help so far already! – user1796440 Aug 28 '15 at 11:37