IFilterProvider и разделение задач
-
13-12-2019 - |
Вопрос
У меня возникла ситуация, когда мне нужно внедрить некоторые зависимости в фильтр действий, а именно, моего настраиваемого поставщика авторизации в мой настраиваемый атрибут авторизации.Я наткнулся на множество людей и сообщений, которые говорили, что нам следует отделить «метаданные атрибутов» от «поведения».Это имеет смысл, а также тот факт, что атрибуты фильтра не создаются с помощью DependencyResolver, поэтому сложно внедрить зависимости.
Итак, я провел небольшой рефакторинг своего кода и хотел знать, правильно ли я его сделал (я использую Castle Windsor в качестве DI-фреймворка).
Прежде всего я удалил свой атрибут, чтобы он содержал только те необработанные данные, которые мне нужны.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAuthorizeAttribute : Attribute
{
public string Code { get; set; }
}
Я создал собственный фильтр авторизации, который будет содержать логику определения того, имеет ли текущий пользователь надлежащую авторизацию.
public class MyAuthorizationFilter : IAuthorizationFilter
{
private IAuthorizationProvider _authorizationProvider;
private string _code;
public MyAuthorizationFilter(IAuthorizationProvider authorizationProvider, string code)
{
Contract.Requires(authorizationProvider != null);
Contract.Requires(!string.IsNullOrWhiteSpace(code));
_authorizationProvider = authorizationProvider;
_code = code;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (filterContext.HttpContext.Request.IsAuthenticated)
{
BaseController controller = filterContext.Controller as BaseController;
if (controller != null)
{
if (!IsAuthorized(controller.CurrentUser, controller.GetCurrentSecurityContext()))
{
// forbidden
filterContext.RequestContext.HttpContext.Response.StatusCode = 403;
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new RedirectToRouteResult("default", new RouteValueDictionary(new
{
action = "http403",
controller = "error"
}), false);
}
else
{
filterContext.Result = controller.InvokeHttp404(filterContext.HttpContext);
}
}
}
else
{
}
}
else
{
filterContext.Result = new RedirectResult(FormsAuthentication.LoginUrl);
}
}
private bool IsAuthorized(MyUser user, BaseSecurityContext securityContext)
{
bool has = false;
if (_authorizationProvider != null && !string.IsNullOrWhiteSpace(_code))
{
if (user != null)
{
if (securityContext != null)
{
has = _authorizationProvider.HasPermission(user, _code, securityContext);
}
}
}
else
{
has = true;
}
return has;
}
}
Последняя часть заключалась в создании пользовательского поставщика фильтров, который будет извлекать этот конкретный атрибут и создавать экземпляр моего пользовательского фильтра, передавая его зависимости и любые необходимые данные, извлеченные из атрибута.
public class MyAuthorizationFilterProvider : IFilterProvider
{
private IWindsorContainer _container;
public MyAuthorizationFilterProvider(IWindsorContainer container)
{
Contract.Requires(container != null);
_container = container;
}
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
Type controllerType = controllerContext.Controller.GetType();
var authorizationProvider = _container.Resolve<IAuthorizationProvider>();
foreach (MyAuthorizeAttribute attribute in controllerType.GetCustomAttributes(typeof(MyAuthorizeAttribute), false))
{
yield return new Filter(new MyAuthorizationFilter(authorizationProvider, attribute.Code), FilterScope.Controller, 0);
}
foreach (MyAuthorizeAttribute attribute in actionDescriptor.GetCustomAttributes(typeof(MyAuthorizeAttribute), false))
{
yield return new Filter(new MyAuthorizationFilter(authorizationProvider, attribute.Code), FilterScope.Action, 0);
}
}
}
Последний шаг — регистрация поставщика фильтра в файле global.asax.
FilterProviders.Providers.Add(new MyAuthorizationFilterProvider(_container));
Поэтому мне интересно, во-первых, правильно ли я понял идею, а во-вторых, что можно улучшить.
Решение
Да, я думаю, вы правильно поняли идею.Мне нравится, что вы разделяете задачи между атрибутом и реализацией фильтра, и мне нравится, что вы используете DI конструктора, а не DI свойства.
Ваш подход хорошо работает, если у вас есть только один тип фильтра.Я думаю, что самой большой потенциальной областью для улучшения, если бы у вас было более одного типа фильтров, было бы то, как реализован поставщик фильтров.В настоящее время поставщик фильтров тесно связан с предоставляемыми им атрибутами и экземплярами фильтров.
Если вы хотите объединить атрибут с фильтром и использовать свойство DI, есть простой способ получить более несвязанный поставщик фильтров.Вот два примера такого подхода:http://www.thecodinghumanist.com/blog/archives/2011/1/27/structuremap-action-filters-and-dependent-injection-in-asp-net-mvc-3 http://lozanotek.com/blog/archive/2010/10/12/dependent_injection_for_filters_in_mvc3.aspx
При нынешнем подходе необходимо решить две проблемы:1.Внедрение некоторых, но не всех параметров конструктора фильтра через DI.2.Сопоставление атрибута с экземпляром фильтра (с внедрением зависимостей).
В настоящее время вы делаете и то, и другое вручную, что вполне нормально, если имеется только один фильтр/атрибут.Если бы их было больше, вам, вероятно, потребовался бы более общий подход для обеих частей.
Для задачи №1 вы можете использовать что-то вроде перегрузки _container.Resolve, которая позволяет передавать аргументы.Это решение довольно специфично для контейнера и, вероятно, немного сложное.
Другое решение, которое я опишу здесь, выделяет фабричный класс, который принимает в своем конструкторе только зависимости, и создает экземпляр фильтра, требующий как DI, так и не DI аргументов.
Вот как может выглядеть эта фабрика:
public interface IFilterInstanceFactory
{
object Create(Attribute attribute);
}
Затем вы реализуете фабрику для каждой пары атрибут/фильтр:
public class MyAuthorizationFilterFactory : IFilterInstanceFactory
{
private readonly IAuthorizationProvider provider;
public MyAuthorizationFilterFactory(IAuthorizationProvider provider)
{
this.provider = provider;
}
public object Create(Attribute attribute)
{
MyAuthorizeAttribute authorizeAttribute = attribute as MyAuthorizeAttribute;
if (authorizeAttribute == null)
{
return null;
}
return new MyAuthorizationFilter(provider, authorizeAttribute.Code);
}
}
Вы можете решить проблему №2, просто зарегистрировав каждую реализацию IFilterInstanceFactory в CastleWindsor.
Поставщик фильтров теперь может быть отделен от каких-либо знаний о конкретных атрибутах и фильтрах:
public class MyFilterProvider : IFilterProvider
{
private IWindsorContainer _container;
public MyFilterProvider(IWindsorContainer container)
{
Contract.Requires(container != null);
_container = container;
}
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
Type controllerType = controllerContext.Controller.GetType();
var authorizationProvider = _container.Resolve<IAuthorizationProvider>();
foreach (FilterAttribute attribute in controllerType.GetCustomAttributes(typeof(FilterAttribute), false))
{
object instance = Resolve(attribute);
yield return new Filter(instance, FilterScope.Controller, 0);
}
foreach (FilterAttribute attribute in actionDescriptor.GetCustomAttributes(typeof(FilterAttribute), false))
{
object instance = Resolve(attribute);
yield return new Filter(instance, FilterScope.Action, 0);
}
}
private object Resolve(Attribute attribute)
{
IFilterInstanceFactory[] factories = _container.ResolveAll<IFilterInstanceFactory>();
foreach (IFilterInstanceFactory factory in factories)
{
object dependencyInjectedInstance = factory.Create(attribute);
if (dependencyInjectedInstance != null)
{
return dependencyInjectedInstance;
}
}
return attribute;
}
}
Дэйвид
Другие советы
Это, вероятно, немного сильно, но один из способов избежать фабрики, предложенного David (и сделать это немного более общего) - это представить еще один атрибут.
[AssociatedFilter(typeof(MyAuthorizationFilter))]
.
Что вы можете добавить в исходный атрибут следующим образом.
[AssociatedFilter(typeof(MyAuthorizationFilter))]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAuthorizeAttribute : Attribute
{
public string Code { get; set; }
}
.
Атрибут StatingFilter выглядит так.
public class AssociatedFilterAttribute : Attribute
{
public AssociatedFilterAttribute(Type filterType)
{
FilterType = filterType;
}
public Type FilterType { get; set; }
}
.
Тогда вы можете получить правильный фильтр, вытягивая фильтртип из этого атрибута.
private object Resolve(Attribute attribute)
{
var filterAttributes = attribute.GetType().GetCustomAttributes(typeof(AssociatedFilterAttribute), false);
var first = (AssociatedFilterAttribute)filterAttributes.FirstOrDefault();
return new Filter(_container.Resolve(first.FilterType), FilterScope.First, null);
}
.
В настоящее время это ограничено только первым атрибутом ConningsFilter, теоретически я думаю, что вы можете добавить более одного (один атрибут выключается несколько фильтров), в этом случае вы пропустите бит, где это захватывает первый результат.
.Очевидно, нам также нужно добавить обработку ошибок, напримерЕсли нет связанногоFilterattribute ...