IFilterProvider e separazione delle preoccupazioni

Ho una situazione in cui ho bisogno di iniettare alcune dipendenze in un filtro azione, vale a dire il mio fornitore di authorization personalizzato nel mio attributo di authorization personalizzato. Mi sono imbattuto in un sacco di persone e post che dicevano che dovremmo separare i “metadati degli attributi” dal “comportamento”. Questo ha senso e c’è anche il fatto che gli attributi del filtro non sono istanziati attraverso il ‘DependencyResolver’, quindi è difficile iniettare le dipendenze.

Così ho fatto un po ‘di refactoring del mio codice e volevo sapere se avevo ragione (sto usando Castle Windsor come framework DI).

Prima di tutto ho spogliato il mio attributo per contenere solo i dati grezzi di cui ho bisogno

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class MyAuthorizeAttribute : Attribute { public string Code { get; set; } } 

Ho creato un filtro di authorization personalizzato che conterrebbe la logica per determinare se l’utente corrente dispone dell’authorization appropriata

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

L’ultima parte era la creazione di un fornitore di filtri personalizzati che recuperava questo specifico attributo e creava un’istanza del filtro personalizzato che trasmetteva le sue dipendenze e tutti i dati necessari, estratti dall’attributo.

 public class MyAuthorizationFilterProvider : IFilterProvider { private IWindsorContainer _container; public MyAuthorizationFilterProvider(IWindsorContainer container) { Contract.Requires(container != null); _container = container; } public IEnumerable GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) { Type controllerType = controllerContext.Controller.GetType(); var authorizationProvider = _container.Resolve(); 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); } } } 

L’ultimo passo è registrare il provider del filtro nel global.asax

 FilterProviders.Providers.Add(new MyAuthorizationFilterProvider(_container)); 

Quindi mi chiedo in primo luogo, se ho avuto l’idea giusta e seconda, cosa potrebbe essere migliorato.

Sì, penso che tu abbia avuto l’idea giusta. Mi piace che tu stia separando le preoccupazioni tra l’attributo e l’implementazione del filtro, e mi piace il fatto che tu stia usando la DI piuttosto che la proprietà DI.

Il tuo approccio funziona bene se hai solo un tipo di filtro. Penso che la più grande area di miglioramento potenziale, se avessi più di un tipo di filtro, sarebbe come implementare il provider di filtri. Attualmente, il fornitore di filtri è strettamente collegato all’attributo e alle istanze di filtro che fornisce.

Se sei disposto a combinare l’attributo con il filtro e utilizzare la proprietà DI, c’è un modo semplice per avere un fornitore di filtri più disaccoppiato. Ecco due esempi di questo approccio: http://www.thecodinghumanist.com/blog/archives/2011/1/27/structuremap-action-filters-and-dependency-injection-in-asp-net-mvc-3 http : //lozanotek.com/blog/archive/2010/10/12/dependency_injection_for_filters_in_mvc3.aspx

Ci sono due sfide da risolvere con l’approccio attuale: 1. Iniettare alcuni, ma non tutti, i parametri del costruttore del filtro tramite DI. 2. Mappatura da un attributo a un’istanza del filtro (dipendente dall’iniezione).

Al momento, lo stai facendo manualmente, il che è sicuramente corretto quando c’è un solo filtro / attributo. Se ce ne fossero di più, probabilmente vorresti un approccio più generale per entrambe le parti.

Per la prima sfida, potresti usare qualcosa come un _container.Resolve overload che ti permette di passare argomenti. Questa soluzione è piuttosto specifica per il contenitore e probabilmente un po ‘complicata.

Un’altra soluzione, che descriverò qui, separa una class factory che accetta solo dipendenze nel suo costruttore e produce un’istanza di filtro che richiede sia argomenti DI che non-DI.

Ecco come potrebbe essere quella fabbrica:

 public interface IFilterInstanceFactory { object Create(Attribute attribute); } 

Dovresti quindi implementare una factory per ogni attributo / coppia di filtri:

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

Puoi risolvere la sfida n. 2 registrando ogni implementazione di IFilterInstanceFactory con CastleWindsor.

Il fornitore di filtri può ora essere disaccoppiato da qualsiasi conoscenza di attributi e filtri specifici:

 public class MyFilterProvider : IFilterProvider { private IWindsorContainer _container; public MyFilterProvider(IWindsorContainer container) { Contract.Requires(container != null); _container = container; } public IEnumerable GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) { Type controllerType = controllerContext.Controller.GetType(); var authorizationProvider = _container.Resolve(); 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(); foreach (IFilterInstanceFactory factory in factories) { object dependencyInjectedInstance = factory.Create(attribute); if (dependencyInjectedInstance != null) { return dependencyInjectedInstance; } } return attribute; } } 

David

Questo è probabilmente un po ‘troppo, ma un modo per evitare la fabbrica come suggerito da David (e renderlo un po’ più generico) è introdurre un altro attributo.

 [AssociatedFilter(typeof(MyAuthorizationFilter))] 

Quale potresti aggiungere all’attributo originale come segue.

 [AssociatedFilter(typeof(MyAuthorizationFilter))] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class MyAuthorizeAttribute : Attribute { public string Code { get; set; } } 

L’attributo AssociatedFilter ha questo aspetto.

 public class AssociatedFilterAttribute : Attribute { public AssociatedFilterAttribute(Type filterType) { FilterType = filterType; } public Type FilterType { get; set; } } 

Quindi puoi recuperare il filtro corretto estraendo FilterType da questo attributo.

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

Attualmente questo è limitato a prendere solo il primo attributo AssociatedFilter, teoricamente suppongo che potresti aggiungere più di uno (un attributo dà il via a diversi filtri), nel qual caso dovresti omettere il bit in cui questo preleva il primo risultato.

Ovviamente abbiamo anche bisogno di aggiungere la gestione degli errori, ad esempio se non ci sono AssociatedFilterAttribute …