问题 使用静态工厂Func 为ASP.NET应用程序创建“Ambient Context”(UserContext)


我发现我几乎每个类(控制器,视图,HTML帮助程序,服务等)都需要当前登录的用户数据。所以我想创建一个“环境上下文”而不是直接注入IUserService或User。

我的方法看起来像那样。

public class Bootstrapper
{
    public void Boot()
    {
        var container = new Container();
        // the call to IUserService.GetUser is cached per Http request
        // by using a dynamic proxy caching mechanism, that also handles cases where we want to 
        // invalidate a cache within an Http request
        UserContext.ConfigureUser = container.GetInstance<IUserService>().GetUser;
    }
}

public interface IUserService
{
    User GetUser();
}

public class User
{
    string Name { get; set; }
}

public class UserContext : AbstractFactoryBase<User>
{
    public static Func<User> ConfigureUser = NotConfigured;

    public static User ActiveUser { get { return ConfigureUser(); } }
}

public class AbstractFactoryBase<T>
{
    protected static T NotConfigured()
    {
        throw new Exception(String.Format("{0} is not configured", typeof(T).Name));
    }
}

用法示例:

public class Controller
{
     public ActionResult Index()
     {
         var activeUser = UserContext.ActiveUser;
         return View();
     }
}

我的方法是正确的还是我错过了什么?你有更好的解决方案吗?

更新: 

更多细节 用户 类:

public class User
{
   string Name { get; set; }
   bool IsSuperUser { get; set;}
   IEnumerable<AzManOperation> Operations { get; set}
}

控制器 我们需要检查用户是否是超级用户才能为超级用户提供一些额外的功能。

public class BaseController : Controller
{
    private readonly IUserService _userService;

    BaseControler(IUserService userService)
    {
        _userService = userService
    }

    public User ActiveUser
    {
        get { return _userService.GetUser(); }
    }
}

查看 如果用户有权这样做,我们会检查操作以仅显示编辑或删除按钮。视图从不使用DependencyResolver,而是使用ViewBag或ViewModel。我的想法是实现自定义ViewBasePage并提供ActiveUser属性,以便Views可以轻松访问。

HtmlHelpers 我们根据IsSuperUser和Operations渲染控件(传入User对象或使用DependencyResolver)。

服务类 我们也需要那些属性。例如,判断篮子是否有效(检查是否允许用户购买不在标准列表中的文章)。所以Service类依赖于 IUserService 并打电话 GetUser()

动作过滤器 强制用户更改密码(仅当它不是SuperUser并且User.ForcePasswordChange为true时)。这里我们使用DependencyResolver。

我希望有一个更简单的方法来获取User对象,而不是使用DependencyResolver.Current.GetService()。GetUser()或使用像 ViewBag.ActiveUser = User。 User对象是几乎到处检查权限等所需的对象。


11317
2018-06-09 15:56


起源

在使用Ambient Context之前,我会先看看我的所有选择。看到优秀的书 .NET中的依赖注入。 - TrueWill
我已经拥有了这本书。正如我所提到的,我几乎在应用程序的任何地方都需要用户,所以我认为开始使用Ambient Context而不是使用构造函数注入或服务位置可能没问题。 - Rookian
如果您“几乎每个班级都需要当前登录的用户数据”,您可能正在考虑一个跨领域的关注点。您是否考虑过装饰器,动态拦截器或过滤器? - Mark Seemann
@Rookian我不能(还)帮助你,因为我不知道你想要做什么。你能解释为什么你需要“控制器,视图,HTML助手,服务等”中的用户数据吗?我可以理解控制器,但视图,HTML帮助器,服务等?这听起来像是对我缺乏凝聚力...... - Mark Seemann
我的想法与@MarkSeemann的想法相同。如果您在“每个班级”中都需要该服务,则表明您的设计存在问题。特别是在View或Html助手中需要服务的味道很糟糕。你不应该需要任何依赖。但是,对于我们提供任何有用的反馈,您需要显示一些视图示例,html帮助器和控制器,以显示您在那里使用该服务的方式。 - Steven


答案:


在视图中,如果用户有权这样做,我们会检查操作以仅显示编辑或删除按钮。

视图不应该进行此检查。 Controller应该将视图模型返回到包含布尔属性的视图,该属性指出这些按钮是否应该可见。回来一个布尔 IsSuperUser 已经转移到了众所周知的视野中。视图不应该知道它应该为超级用户显示某个按钮:这取决于控制器。应该只告知视图要显示的内容。

如果几乎所有视图都有此代码,则有一些方法可以从视图中提取重复的部分,例如使用部分视图。如果您发现自己在许多视图模型上重复这些属性,也许您应该定义一个包络视图模型(将特定模型包装为的通用视图模型) T)。控制器可以创建其视图模型,同时创建服务或横切关注点,将其包装在信封中。

在服务类中,我们也需要这些属性。例如,决定篮子是否有效

在这种情况下,您谈论的是验证,这是一个跨领域的问题。您应该使用装饰器来添加此行为。


8
2018-06-10 21:02



那么代替User.IsSuperUser,ViewModel.ShowCreateButton,对吗?因为映射的东西,我害怕过度复杂化。但我明白你的观点。我根本不喜欢基本视图模型的想法。此外,基本视图模型非常胖,因为当不允许使用IsSuperUser之类的东西时,它需要为很多视图服务,因此我需要一个ViewModel.ShowFavListCreateButton,ViewModel.ShowCreateAdressListButton。这两个按钮都不是User.IsSuperUser。当我不被允许共享User.IsSuperUser时,一个模型的想法无法工作。 - Rookian
我不明白我的服务类的装饰器。你能举一个简单的例子,我能说明你的观点吗? - Rookian
@Rookian:你熟悉吗? 本文 我的?它讨论了将业务操作隐藏在一个通用的背后 ICommandHandler<T> 抽象。执行此操作时,您可以轻松创建包装这些业务操作的装饰器,例如执行验证的装饰器。 - Steven
我不熟悉你的文章。该链接不起作用。 - Rookian
@Rookian:链接现在应该可以使用了。 - Steven


这是MVC,对吧?

你正在重新发明轮子。

将此方法添加到Global.asax.cs:

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
    var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {
        var ticket = FormsAuthentication.Decrypt(authCookie.Value);
        var user = ticket.Name;
        var identity = new GenericIdentity(user, "Forms");
        var principal = new GenericPrincipal(identity, null);
        Context.User = principal;
    }
}

此示例显示了表单身份验证,如果您正在使用其他机制,则可以将其删除。关键是这三行:

    var identity = new GenericIdentity(user, "Forms");
    var principal = new GenericPrincipal(identity, null);
    Context.User = principal;

GenericIdentity和GenericPrincipal可以替换为您想要的任何东西,只要它们实现(平凡的)IIdentity和IPrincipal接口即可。您可以使用所需的任何额外属性创建自己的这些类的实现。

然后,您可以通过HttpContext.Current.User(静态)从您列出的所有内容(控制器,视图等)访问经过身份验证的用户。

如果您创建了自己的IPrincipal实现,则可以将该引用强制转换为自定义类型。

你会注意到IPrincipal有一个名为IsInRole的方法,所以你会说:

if (HttpContext.Current.User.IsInRole("SuperUser"))

TL; DR  - 你已经解决了ASP.NET已经解决的问题,如果我看到你在生产应用程序中提出的类型,我会有动脉瘤。


2
2018-06-19 17:49



我需要为用户提供的所有其他属性是什么?它不仅是用户名和IsInRole。 - Rookian
Context.User是对IPrincipal的引用。您可以根据需要使用任意数量的属性创建所需的任何类型,并将其分配给User属性,只要它还实现IPrincipal即可。当您需要访问其他属性时,您可以在访问点转换Context.User属性。您还可以创建一个类型化的扩展方法来为您执行强制类型转换,如果您是那些认为强制转换表明设计缺陷的人之一,则返回强类型引用。 - Evan Machusak


我认为最简单和可维护的解决方案是创建一个静态类CurrentUserProvider,它只有一个方法Get(HttpContextBase)返回当前用户,在场景后面你可以使用DependencyResolver来获得实际返回用户的服务。然后,在需要CurrentUser的地方,您可以调用CurrentUserProvider.Get(上下文)并执行您需要执行的任何自定义逻辑。

你要做的另一个解决方案是在基本控制器构造函数中注入服务,如果你有一些控制器就可以了,如果你有很多控制器并且并非所有的控制器都需要这个服务器,这将成为一个问题。为这些控制器编写测试将是如此痛苦,因为您必须为所有控制器测试为该服务创建存根/模拟。也许你可以使用属性注入而不是构造函数来解决它。

您也可以对过滤器使用相同的属性注入。

现在,剩下的两个是视图和帮助器。对于View,您可以创建从WebViewPage / ViewPage继承的特殊基类,并使用IViewActivator注入服务,同样适用于帮助程序,创建从系统助手继承的助手,并在基本控制器和视图中使用这些助手。

我认为第二种方法有点麻烦,并没有为所有那些自定义的东西增加那么多的价值。

所以我的建议是第一个。


0
2018-06-18 10:15