Pergunta

Tenho uma situação em que preciso injetar algumas dependências em um filtro de ação, ou seja, meu provedor de autorização customizado em meu atributo de autorização customizado.Me deparei com muitas pessoas e postagens que diziam que deveríamos separar os 'metadados de atributos' do 'comportamento'.Isso faz sentido e há também o fato de que os atributos do filtro não são instanciados por meio do 'DependencyResolver', portanto é difícil injetar as dependências.

Então refatorei um pouco meu código e queria saber se estava certo (estou usando o Castle Windsor como estrutura de DI).

Primeiro, eliminei meu atributo para conter apenas os dados brutos necessários

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

Criei um filtro de autorização personalizado que conteria a lógica para determinar se o usuário atual tem a autorização adequada

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

A última parte foi criar um provedor de filtro personalizado que buscaria esse atributo específico e instanciaria meu filtro personalizado, passando suas dependências e quaisquer dados necessários, extraídos do atributo.

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

A última etapa é registrar o provedor de filtro no global.asax

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

Então, estou me perguntando, primeiro, se entendi bem a ideia e, segundo, o que poderia ser melhorado.

Foi útil?

Solução

Sim, acho que você acertou a ideia.Gosto que você esteja separando as preocupações entre o atributo e a implementação do filtro e gosto que esteja usando o construtor DI em vez da propriedade DI.

Sua abordagem funciona bem se você tiver apenas um tipo de filtro.Acho que a maior área potencial de melhoria, se você tivesse mais de um tipo de filtro, seria como o provedor do filtro é implementado.Atualmente, o provedor de filtro está fortemente acoplado ao atributo e às instâncias de filtro que ele está fornecendo.

Se você deseja combinar o atributo com o filtro e usar a propriedade DI, há uma maneira simples de ter um provedor de filtro mais desacoplado.Aqui estão dois exemplos dessa abordagem: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

Existem dois desafios a resolver com a abordagem atual:1.Injetando alguns, mas não todos, parâmetros do construtor de filtro via DI.2.Mapeamento de um atributo para uma instância de filtro (injetada por dependência).

Atualmente, você está fazendo as duas coisas manualmente, o que certamente é bom quando há apenas um filtro/atributo.Se houvesse mais, você provavelmente desejaria uma abordagem mais geral para ambas as partes.

Para o desafio nº 1, você pode usar algo como uma sobrecarga _container.Resolve que permite passar argumentos.Essa solução é bastante específica do contêiner e provavelmente um pouco complicada.

Outra solução, que descreverei aqui, separa uma classe de fábrica que aceita apenas dependências em seu construtor e produz uma instância de filtro que requer argumentos DI e não-DI.

Esta é a aparência dessa fábrica:

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

Você implementaria então uma fábrica para cada par atributo/filtro:

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

Você pode resolver o desafio nº 2 apenas registrando cada implementação de IFilterInstanceFactory com CastleWindsor.

O provedor de filtros agora pode ser dissociado de qualquer conhecimento de atributos e filtros específicos:

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

Davi

Outras dicas

Provavelmente isso é um pouco demais, mas uma maneira de evitar a fábrica sugerida por David (e tornar isso um pouco mais genérico) é introduzir outro atributo.

[AssociatedFilter(typeof(MyAuthorizationFilter))]

Que você pode adicionar ao atributo original da seguinte maneira.

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

O atributo AssociatedFilter se parece com isto.

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

Então você pode recuperar o filtro correto retirando o FilterType deste atributo.

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

Atualmente, isso está limitado a apenas usar o primeiro atributo AssociatedFilter; teoricamente, acho que você poderia adicionar mais de um (um atributo inicia vários filtros); nesse caso, você omitiria o bit em que obtém o primeiro resultado.

Obviamente, também precisamos adicionar tratamento de erros, por ex.se não houver AssociatedFilterAttribute...

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top