Passive Attributes and Nested Containers

2019-01-29 10:36发布

Final Solution

With help from @NightOwl888's answer, here's the final approach I went with for anyone who ends up here:

1) Added the global filter provider:

public class GlobalFilterProvider : IFilterProvider
{
    public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        var nestedContainer = StructuremapMvc.StructureMapDependencyScope.CurrentNestedContainer;

        foreach (var filter in nestedContainer.GetAllInstances<IActionFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }

        foreach (var filter in nestedContainer.GetAllInstances<IAuthorizationFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }

        foreach (var filter in nestedContainer.GetAllInstances<IExceptionFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }

        foreach (var filter in nestedContainer.GetAllInstances<IResultFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }

        foreach (var filter in nestedContainer.GetAllInstances<IAuthenticationFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }
    }
}

2) Registered it in the FilterProviders collection:

public static void Application_Start()
{
    // other bootstrapping code...

    FilterProviders.Providers.Insert(0, new GlobalFilterProvider());
}

3) Added a custom filter using the passive attributes approach:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class SomeAttribute : Attribute
{
}

public class SomeFilter : IActionFilter
{
    private readonly ISomeDependency _dependency;

    public SomeFilter(ISomeDependency dependency)
    {
        _dependency = dependency;
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (!filterContext.ActionDescriptor.GetCustomAttributes(true).OfType<SomeAttribute>().Any())
            return;

        _dependency.DoWork();
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
    }
}

4) Then wired everything up in StructureMap (in this solution the SomeAttribute and GlobalFilterProvider classes are in the same "Filters" folder within the root folder):

public class ActionFilterRegistry : Registry
{
    public ActionFilterRegistry()
    {
        Scan(s =>
        {
            // find assembly containing custom filters
            s.AssemblyContainingType<GlobalFilterProvider>();

            // limit it to the folder containing custom filters
            s.IncludeNamespaceContainingType<GlobalFilterProvider>();

            // exclude any of the Attribute classes that contain metadata but not the behavior
            s.Exclude(type => type.IsSubclassOf(typeof(Attribute)));

            // register our custom filters
            s.AddAllTypesOf<IActionFilter>();
        });
    }
}

Original Post

I'm currently using a nested container per request with StructureMap in an ASP.NET MVC 5 application. I'm utilizing the structuremap.mvc5 nuget package to setup all the DI infrastructure for me (the dependency resolver, wiring up the container and creating and disposing of the nested container on App_BeginRequest and App_EndRequest). I'm at the point now where I need to do some DI within action filters in order to automate some functionality. After a good amount of research, I am attempting to do so without the need for setter injection, using Mark Seemann's passive attributes approach.

All seemed well and good while building the attribute and filter, until I got to registering the filter with the global filter collection within App_Start. I have a dependency that I would like to be created only once per request so that not only the action filter, but also other non-filter infrastructure classes utilized during a request, can use the same instance of that dependency over the entire request. If the nested container were resolving the dependency, it would do that by default. However, because I have to register the new filter in App_Start, I don't have access to the nested container.

For example, my global.asax:

public class MvcApplication : System.Web.HttpApplication
{
    public static StructureMapDependencyScope StructureMapDependencyScope { get; set; }

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);

        var container = IoC.Initialize(); // contains all DI registrations
        StructureMapDependencyScope = new StructureMapDependencyScope(container);
        DependencyResolver.SetResolver(StructureMapDependencyScope);

        // filter uses constructor injection, so I have to give it an instance in order to new it up, 
        // but nested container is not available
        GlobalFilters.Filters.Add(new SomeFilter(container.GetInstance<ISomeDependency>()));
    }

    protected void Application_BeginRequest()
    {
        StructureMapDependencyScope.CreateNestedContainer();
    }

    protected void Application_EndRequest()
    {
        HttpContextLifecycle.DisposeAndClearAll();
        StructureMapDependencyScope.DisposeNestedContainer();
    }

    protected void Application_End()
    {
        StructureMapDependencyScope.Dispose();
    }
}

Does anyone know how to solve this? I've come across the decoraptor solution as well via this other SO question, but using an abstract factory within my filter would just create a new instance of the dependency, rather than using the single per request instance I want.

The only other solution I have come up with was to use setter injection with a custom filter provider that uses the static StructureMapDependencyScope instance created in the global, like this:

public class StructureMapFilterProvider : FilterAttributeFilterProvider
{
    public override IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        var filters = base.GetFilters(controllerContext, actionDescriptor);

        foreach (var filter in filters)
        {
            MvcApplication.StructureMapDependencyScope.CurrentNestedContainer.BuildUp(filter.Instance);
        }

        return filters;
    }
}

While that seems to work alright, it just seems a little dirty.

1条回答
闹够了就滚
2楼-- · 2019-01-29 11:27

You can build a custom filter provider (as in this answer) to control the lifetime of the filters, rather than registering them in the static GlobalFilters.Filters collection.

public class GlobalFilterProvider : IFilterProvider
{
    public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        var nestedContainer = StructuremapMvc.StructureMapDependencyScope.CurrentNestedContainer;

        foreach (var filter in nestedContainer.GetAllInstances<IActionFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }

        foreach (var filter in nestedContainer.GetAllInstances<IAuthorizationFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }

        foreach (var filter in nestedContainer.GetAllInstances<IExceptionFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }

        foreach (var filter in nestedContainer.GetAllInstances<IResultFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }

        foreach (var filter in nestedContainer.GetAllInstances<IAuthenticationFilter>())
        {
            yield return new Filter(filter, FilterScope.Global, order: null);
        }
    }
}

Usage

Keep in mind, MVC already contains lots of filter types. Even implementations of the base Controller type are registered as global filters, as Controller implements every type of filter. So, you need to be precise when you register your custom global filter types.

Option 1: Using convention based registration

// Register the filter provider with MVC.
FilterProviders.Providers.Insert(0, new GlobalFilterProvider());

Then in your DI registration

Scan(_ =>
{
    // Declare which assemblies to scan
    // In this case, I am assuming that all of your custom
    // filters are in the same assembly as the GlobalFilterProvider.
    // So, you need to adjust this if necessary.
    _.AssemblyContainingType<GlobalFilterProvider>();

    // Optional: Filter out specific MVC filter types
    _.Exclude(type => type.Name.EndsWith("Controller"));

    // Add all filter types.
    _.AddAllTypesOf<IActionFilter>();
    _.AddAllTypesOf<IAuthorizationFilter>();
    _.AddAllTypesOf<IExceptionFilter>();
    _.AddAllTypesOf<IResultFilter>();
    _.AddAllTypesOf<IAuthenticationFilter>(); // MVC 5 only
});

NOTE: You can control which filters MVC registers by changing the IFilterProvider instances that are registered.

enter image description here

So, an alternative could be something like:

FilterProviders.Providers.Clear();

// Your custom filter provider
FilterProviders.Providers.Add(new GlobalFilterProvider());

// This provider registers any filters in GlobalFilters.Filters
FilterProviders.Providers.Add(new System.Web.Mvc.GlobalFilterCollection());

// This provider registers any FilterAttribute types automatically (such as ActionFilterAttribute)
FilterProviders.Providers.Insert(new System.Web.Mvc.FilterAttributeFilterCollection());

Since the above code does not register the System.Web.Mvc.ControllerInstanceFilterProvider, the Controllers themselves won't be registered as global filters, so you wouldn't need to filter them out. Instead, you could simply let all of your controllers be registered as global filters.

// Optional: Filter out specific MVC filter types
// _.Exclude(type => type.Name.EndsWith("Controller"));

Option 2: Individual registration

// Register the filter provider with MVC.
FilterProviders.Providers.Insert(0, new GlobalFilterProvider());

Then in your DI registration

For<IActionFilter>().Use<MyGlobalActionFilter>();
For<IActionFilter>().Use<MyOtherGlobalActionFilter>();
查看更多
登录 后发表回答