Создание "Ambient Context" (UserContext) для приложения ASP.NET с использованием статической фабрики Func<T>
Я обнаружил, что мне нужны текущие данные о пользователях, вошедших в систему, практически в каждом классе (контроллеры, представления, помощники HTML, службы и т. Д.). Поэтому я подумал о создании "окружающего контекста" вместо непосредственного введения IUserService или пользователя.
Мой подход выглядит примерно так.
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, чтобы представления имели легкий доступ.
В HtmlHelpers мы визуализируем элементы управления в зависимости от IsSuperUser и Operations (передавая объект User или используя DependencyResolver).
В классах обслуживания нам также нужны эти свойства. Например, чтобы решить, является ли корзина действительной или нет (проверьте, разрешено ли пользователю покупать товары, которых нет в стандартном списке). Таким образом, класс обслуживания зависит от IUserService
и звонит GetUser()
,
В Action Filters заставить пользователя сменить свой пароль (только если это не SuperUser и User.ForcePasswordChange имеет значение true). Здесь мы используем DependencyResolver.
Мое желание состоит в том, чтобы иметь более простой способ получить объект User вместо использования DependencyResolver.Current.GetService().GetUser() или использования таких вещей, как ViewBag.ActiveUser = User
, Объект User - это объект, который почти везде необходим для проверки прав доступа или тому подобного.
3 ответа
В представлениях мы отмечаем Операции, чтобы отображать кнопку редактирования или удаления только в том случае, если пользователь имеет на это право.
По мнению не следует делать эту проверку. Контроллер должен вернуть модель представления в представление, которое содержит логические свойства, определяющие, должны ли эти кнопки быть видимыми. Возвращая бул с IsSuperUser
уже переходит к многим известным знаниям в поле зрения. Представление не должно знать, что оно должно показывать определенную кнопку для суперпользователя: это зависит от контроллера. Вид должен только сказать, что отображать.
Если почти все представления имеют этот код, есть способы извлечь повторяющиеся части из ваших представлений, например, с частичными представлениями. Если вы обнаружите, что повторяете эти свойства во многих моделях представления, возможно, вам следует определить модель представления конверта (универсальная модель представления, которая охватывает конкретную модель как T
). Контроллер может создать свою модель представления, в то время как вы создаете сервис или сквозную задачу, которая оборачивает ее в ваш конверт.
В классах обслуживания нам также нужны эти свойства. Например, чтобы решить, является ли корзина действительной или нет
В этом случае вы говорите о проверке, которая является сквозной задачей. Вы должны использовать декораторы, чтобы добавить это поведение вместо.
Это 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 уже решило, и у меня была бы аневризма, если бы я увидел типы, которые вы предлагаете в производственном приложении.
Я думаю, что самое простое и поддерживаемое решение - создать статический класс CurrentUserProvider, который имеет только один метод Get(HttpContextBase), который возвращает текущего пользователя, за сценой вы можете использовать DependencyResolver, чтобы получить сервис, который фактически возвращает пользователя. Затем, где вам нужен CurrentUser, вы можете вызвать CurrentUserProvider.Get(context) и выполнить любую пользовательскую логику, которую вам нужно выполнить.
Другое решение, которое вы пытаетесь сделать, это внедрить службу в конструктор базовых контроллеров, что нормально, если у вас есть несколько контроллеров, это может стать проблемой, если у вас достаточно контроллеров, и не все контроллеры нуждаются в этой услуге., Написание тестов для этих контроллеров было бы такой болью в шею, потому что вы должны создать заглушки / макеты для этого сервиса для всех ваших тестов контроллера. Может быть, вы можете использовать инъекцию свойства вместо конструктора для его решения.
Вы можете использовать ту же инъекцию свойств для фильтров тоже.
Теперь оставшиеся два - это представление и помощник. Для View вы можете создать специальный базовый класс, который наследуется от WebViewPage/ViewPage, и использовать IViewActivator для внедрения службы; то же самое относится и к помощникам, создавать помощники, наследуемые от системных помощников, и использовать их в базовых контроллерах и представлениях.
Я думаю, что второй подход немного громоздок, и он не добавляет особой ценности для выполнения всех этих пользовательских вещей.
Поэтому я предлагаю пойти с первым.