【问题标题】:How to initialize a scoped injected class in ASP.NET Core involving asynchronous calls如何在涉及异步调用的 ASP.NET Core 中初始化作用域注入类
【发布时间】:2020-01-07 17:51:37
【问题描述】:

我需要在 ASP.NET Core 的作用域级别初始化一个注入类 - 初始化涉及异步方法调用。您不会在构造函数中执行此操作,也不会在属性访问器中执行此操作。

在 asp.net 核心应用程序中的一个常见 DI 用途是获取当前用户。我通过创建IUserContext 抽象并在作用域级别注入它来实现这一点:

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

我正在努力找出如何正确初始化 CurrentUser 属性。

由于不再有任何方法可以同步地从UserManager 类中获取用户,因此在初始化CurrentUser 时从属性getter 中运行异步方法,或者从构造函数中运行异步方法(没有使用 ASP.NET Core 的 UserManager 类上的任何同步方法)。

我觉得这样做的正确方法是在每个请求中以某种方式在注入的实例上运行一个初始化方法,因为它是作用域的(可能使用操作过滤器/中间件/控制器基类(或者可能在依​​赖注入中) AddScoped 方法本身就是工厂方法?)

这似乎是一个很常见的问题,我想知道其他人是如何解决这个问题的。

【问题讨论】:

  • 当前用户是Context 上的一个属性 -> 如果您使用的是.Net core auth/approach,这是我推荐的。如果您使用 cookie 或令牌身份验证,则会自动设置
  • 是的,您可以看到我使用 HttpContext 来获取用户的名称。但是,上下文中不存在完整的用户对象(您只能以这种方式检索声明或用户名)。您只能通过调用 UserManger.FindByIdAsync 或直接使用 EFCore 来获取完整的用户对象。
  • 如果需要加载用户,则加载用户。它不是一个可注入的依赖项......我会提供用户主体对象足够灵活,能够包含您应该需要的任何东西 - 如果您需要超越这个,那么也许有更高层次的设计问题
  • 我想通过依赖注入加载用户,以便进行单元测试。因为 UserManager 不再有同步方法来加载它,所以它提出了一个问题。您是将用户加载到注入服务的构造函数中,还是作为属性加载?或者你会从动作过滤器或中间件初始化它吗?那是我的问题。
  • 你会模拟用户管理器和/或将你的程序分成更小的部分。此外,任何异步方法都可以同步运行。

标签: c# asp.net-core dependency-injection


【解决方案1】:

在这种情况下,您需要放弃该属性并使用异步方法。

这也意味着为用户使用异步延迟初始化

/// <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)) { }
}

现在可以重构上下文以使用延迟初始化,

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

并在需要的地方使用

User user = await context.GetCurrentUser();

现在一个属性可以仍然可以像

一样使用
public Task<User> CurrentUser => currentUser.Value;

因为 getter 是一种方法,但在我个人看来这不是一个非常直观的设计。

User user = await context.CurrentUser;

如果过早访问可能会产生不良结果。

我只提到它是因为示例中显示的原始上下文的设计。

【讨论】:

    【解决方案2】:

    你可以试试关注,

    1) 将 IUserContext 和 UserContext 注册为 Scoped

    2) 通过 httpContext 获取 IUSerContext 或 UserContext,

    IUserContext userContext = (IUserContext)httpContext.RequestServices.GetService(typeof(IUserContext));
    

    【讨论】:

      【解决方案3】:

      我个人的做法和nkosi的回答有点不同。

      您的最终要求是访问一个或多个 User 属性,对吧?

      因此,您可以通过将委托与异步混合使用来获得优势:

      public interface IUserAccessor
      {
          Task<TValue> GetValueAsync<TValue>(Func<User, TValue> func);
      }
      
      public class UserAccessor
      {
          public UserAccessor(MyDbContext db)
          {
              this.db = db;
          }
      
          private readonly MyDbContext db;
      
          public async Task<TValue> GetValueAsync<TValue>(Func<User, TValue> func)
          {
              return func(await this.db.FindUsersBySomeConditions().FirstOrDefaultAsync());
          }
      }
      

      如果您的服务是作用域的,您还可以使用持久性并避免再次调用 DB:

      private User user;
      
      public async Task<TValue> GetValueAsync<TValue>(Func<User, TValue> func)
      {
          if (this.user == null)
          {
              this.user = await this.db.FindUsersBySomeConditions().FirstOrDefaultAsync();
          }
      
          return func(this.user);
      }
      

      你可以这样使用:

      @inject IUserService userService
      
      // ...
      
      <div>
          <div>@await userService.GetValueAsync(u => u.Name)</div>
          <div>@await userService.GetValueAsync(u => u.Role)</div>
          <div>@await userService.GetValueAsync(u => u.BirthDate)</div>
          <div>@await userService.GetValueAsync(u => u.ComplexProperty.SubProperty)</div>
      </div>
      

      【讨论】:

        猜你喜欢
        • 2018-04-27
        • 2021-07-21
        • 1970-01-01
        • 1970-01-01
        • 2017-03-23
        • 1970-01-01
        • 1970-01-01
        • 2015-06-19
        • 1970-01-01
        相关资源
        最近更新 更多