Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to initialize a scoped injected class in ASP.NET Core involving asynchronous calls

I need to initialize an injected class at the scope level in ASP.NET Core - the initialization involves asynchronous method calls. You wouldn't do this in the constructor, nor a property accessor.

A common DI use in an asp.net core application is getting the current user. I implemented this by creating an IUserContext abstraction and injecting it at the scoped level:

public sealed class AspNetUserContext : IUserContext
{
    private readonly UserManager<User> userManager;
    private readonly IHttpContextAccessor accessor;
    private User currentUser;

    public AspNetUserContext(IHttpContextAccessor a, UserManager<User> userManager) {
        accessor = a;

        if (userManager == null)
            throw new ArgumentNullException("userManager");

        this.userManager = userManager;
    }

    public string Name => accessor.HttpContext.User?.Identity?.Name;
    public int Id => accessor.CurrentUserId();

    public User CurrentUser {
        get {
            if (currentUser == null) {
                currentUser = this.UserManager.FindByIdAsync(Id.ToString()).ConfigureAwait(false).GetAwaiter().GetResult();
            }

            return currentUser;
        }
    }
}

I am struggling trying to find out how to correctly initialize the CurrentUser property.

Since there is no longer any way to get a user from the UserManager class sychronously, I am not comfortable running an async method from within a property getter when initializing the CurrentUser, nor from the constructor (there are no long any synchronous methods on the UserManager class with ASP.NET Core).

I feel like the correct way to do this would be to run an initialization method on the injected instance somehow once per request since it is scoped (perhaps with an action filter/middleware/Controller base class (or perhaps in the dependency injection AddScoped method itself as a factory method?)

This seems like a pretty common problem and I'm wondering how others have resolved this.

like image 297
John Hamm Avatar asked Jan 07 '20 17:01

John Hamm


1 Answers

In this case you will need to forgo the property and have an asynchronous method.

This would also mean having an asynchronous lazy initialization for the User using

/// <summary>
/// Provides support for asynchronous lazy initialization.
/// </summary>
/// <typeparam name="T"></typeparam>
public class LazyAsync<T> : Lazy<Task<T>> {
    /// <summary>
    ///  Initializes a new instance of the LazyAsync`1 class. When lazy initialization
    ///  occurs, the specified initialization function is used.
    /// </summary>
    /// <param name="valueFactory">The delegate that is invoked to produce the lazily initialized Task when it is needed.</param>
    public LazyAsync(Func<Task<T>> valueFactory) :
        base(() => Task.Run(valueFactory)) { }
}

This now makes it possible to refactor the context to use lazy initialization,

public sealed class AspNetUserContext : IUserContext {
    private readonly UserManager<User> userManager;
    private readonly IHttpContextAccessor accessor;
    private readonly LazyAsync<User> currentUser;

    public AspNetUserContext(IHttpContextAccessor accessor, UserManager<User> userManager) {
        this.accessor = accessor;

        if (userManager == null)
            throw new ArgumentNullException(nameof(userManager));

        this.userManager = userManager;

        currentUser = new LazyAsync<User>(() => this.userManager.FindByIdAsync(Id.ToString()));
    }

    public string Name => accessor.HttpContext.User?.Identity?.Name;
    public int Id => accessor.CurrentUserId();

    public Task<User> GetCurrentUser() {
        return currentUser.Value;
    }
}

And used where needed

User user = await context.GetCurrentUser();

Now a property could have still been used like

public Task<User> CurrentUser => currentUser.Value;

as the getter is a method, but that is not a very intuitive design in my personal opinion.

User user = await context.CurrentUser;

and can have undesirable results if accessed too early.

I only mention it because of the design of the original context shown in the example.

like image 143
Nkosi Avatar answered Sep 20 '22 15:09

Nkosi