如何创建应用程序范围内可访问的上下文对象以在数据访问中存储当前用户详细信息?

How to create context object accessible application-wide to store current user details in data access?

我有一个带有 Web 用户界面、BusinessLogicLayer 和 DataAccessLayer 的 n 层 Web 应用程序。我正在寻找最好的解决方案,将我当前登录的用户的详细信息一直传递到数据访问,而无需将其添加到我的所有方法签名中。出于审计目的,需要用户详细信息。我相信你可以创建一个适用于整个应用程序的上下文,有没有人有这样做的例子?我正在寻找能够分离我的关注点的最佳设计模式。

这里有两种方法:

第一
如果其他层确实需要了解用户,则此方法更有意义。例如,他们可能会检查权限或根据用户身份做出某些决定。

您可以创建一个抽象或接口来描述您希望那些 classes 能够访问的内容,如下所示:

public interface IUserContext
{
    SomeClassContainingUserData GetCurrentUser();
}

我会根据消费者 class 的需求来定义用户数据 class,而不是仅仅使用一些现有的 Customer class 来防止这种情况发生与您的网络应用程序紧密耦合。

现在您可以将 class 注入您的其他 classes:

public class MyBusinessLogicClass
{
    private readonly IUserContext _userContext;

    public MyBusinessLogicClass(IUserContext userContext)
    {
        _userContext = userContext;
    }

    public void SomeOtherMethod(Whatever whatever)
    {
        var user = _userContext.GetCurrentUser();
        // do whatever you need to with that user
    }
}

这可以让您的其他 class 保持可测试性,因为很容易注入您想要的 returns 接口的模拟,这样您就可以确保您的 class 对于不同的行为正确用户类型。

如果您的用户数据来自 HttpContext,那么您的运行时实现大致如下所示:

public class HttpUserContext
{
    private readonly IHttpContextAccessor _contextAccessor;

    public HttpUserContext(IHttpContextAccessor contextAccessor)
    {
        _contextAccessor = contextAccessor;
    }

    public SomeClassContainingUserData GetCurrentUser()
    {
        var httpContext = _contextAccessor.HttpContext;
        // get the user data from the context and return it.
    }
}

这是一个粗略的轮廓。一个考虑因素是范围界定。如果您的所有对象都按请求限定范围,那么 IUserContext 实现可以生成一次用户数据并将其存储在成员变量中,而不是一遍又一遍地访问它。

缺点是必须在任何地方注入它,但如果那些 classes 需要该信息,那是不可避免的。

第二
如果那些内部 classes 实际上根本不需要用户信息怎么办?如果您只想记录哪些用户发出的请求由那些 classes 处理怎么办?如果你想要一个单独的对象来检查权限怎么办?

在那种情况下,一个选项是拦截器或包装器。它的最简单形式可能如下所示:

public class SomeBusinessClassSecurityInterceptor : ISomeBusinessClass
{
    private readonly ISomeBusinessClass _inner;
    private readonly IUserContext _userContext;

    public SomeBusinessClassSecurityInterceptor(
        ISomeBusinessClass inner, IUserContext userContext)
    {
        _inner = inner;
        _userContext = userContext;
    }

    public void UpdateSomeData(Foo data)
    {
        if(!CanUpdate()) 
            throw new YouCantUpdateThisException();
        _inner.UpdateSomeData(data);
    }

    private bool CanUpdate()
    {
        var user = _userContext.GetCurrentUser();
        // make some decision based on the user
    }   
}

如果更复杂地检索用户权限,您可能需要一个 IUserPermissions 并注入它而不是 IUserContext。然后将 IUserContext 注入到 IUserPermissions 的实现中。在运行时,它会检索当前用户,然后自行决定用户拥有哪些权限。

如果您有很多 classes 和方法,那么维护单独的包装器 classes 可能会变得乏味。另一种选择是使用拦截器,这可能意味着使用不同的依赖注入容器,如 Windsor or Autofac。这些特别适合记录日志。

以 Autofac 为例,这意味着要像这样编写 class:

public class LoggingInterceptor : IInterceptor
{
    private readonly IUserContext _userContext;
    private readonly ILogger _logger;

    public CallLogger(IUserContext userContext, ILogger logger)
    {
        _userContext = userContext;
        _logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        var user = _userContext.GetCurrentUser();
        _logger.Log( 
         // some stuff about the invocation, like method name and maybe parameters)
         // and who the user was.
         // Or if you were checking permissions you could throw an exception here.

        invocation.Proceed();
    }
}

然后你会告诉容器所有对给定 class 的 "real" 实现的调用都会通过这个拦截器(在他们的文档中有很好的描述。)

如果您正在检查权限,您可以注入 IUserPermissions。拦截器可以检查内部方法的属性,该属性指定需要什么权限,并将其与当前用户的权限进行比较。

您通常可以编写一个拦截器并将其与许多其他 class 一起使用,因为它不需要了解有关内部目标 class 的任何信息。但是,如果需要,您还可以编写仅用于某些 classes.

的用途更窄的拦截器

它的优点在于它为您提供了很大的灵活性,但不会触及您的业务逻辑或数据的实际接口或实现 classes。他们可以专注于自己的单一职责,而其他 class 被配置为记录向他们发出的请求或检查用户权限。