【转】ASP.NET MVC 3 Service Location, Part 4: Filters

Important Update

We've made significant changes to the IoC support in ASP.NET MVC 3 Beta. Please read Part 5 for more information.

Filters and Filter Providers

We introduced the concept of filters in ASP.NET MVC 1.0. A filter can implement one of more of the following interfaces:IActionFilter, IResultFilter, IExceptionFilter, and IAuthorizationFilter.

In prior versions of MVC, the only way to apply filters was to make a filter attribute and apply that attribute to a controller or an action method. In MVC 3, we've introduced the ability to have filters which are defined outside the scope of attributes (and found via filter providers), as well as a facility for registering global filters.

Update: (31 July 2010) I've added the source code for UnityMvcServiceLocator to the end of part 2.

Disclaimer

This blog post talks about ASP.NET MVC 3 Preview 1, which is a pre-release version. Specific technical details may change before the final release of MVC 3. This release is designed to elicit feedback on features with enough time to make meaningful changes before MVC 3 ships, so please comment on this blog post or contact me if you have comments.

Location: Filter Providers

Filter providers are a new feature to MVC 3. They are a "multiply registered" style service, with a static registration point at FilterProviders.Providers. This collection provides a facade method (GetFilters) which aggregates the filters from all of the providers into a single list. Order of the providers is not important.

By default, there are three filter providers registered into the application:

  • A filter provider for global filters (GlobalFilters.Filters)
  • A filter provider for filter attributes (FilterAttributeFilterProvider)
  • A filter provider for controller instances (ControllerInstanceFilterProvider)

In addition, the GetFilters facade method will also retrieve all the IFilterProvider instances in the service locator by calling MvcServiceLocator.Current.GetAllInstances<IFilterProvider>().

Implementing a Filter Provider

The filter provider interface is defined as follows:

namespace System.Web.Mvc {
    using System.Collections.Generic;

    public interface IFilterProvider {
        IEnumerable<Filter> GetFilters(ControllerContext controllerContext,
                                       ActionDescriptor actionDescriptor);
    }
}

The Filter class is a metadata class that contains a reference to the implementation of one of more of the filter interfaces, plus the filter's order and scope. The class is defined as:

namespace System.Web.Mvc {
    public class Filter {
        public const int DefaultOrder = -1;

        public Filter(object instance, FilterScope scope, int? order = null);

        public object      Instance { get; }
        public int         Order    { get; }
        public FilterScope Scope    { get; }
    }
}

Filter Ordering

Filters are run in a pre-determined order; this behavior has existed in previous versions of MVC, but we have now added an additional scope (global):

namespace System.Web.Mvc {
    public enum FilterScope {
        First = 0,
        Global = 10,
        Controller = 20,
        Action = 30,
        Last = 100,
    }
}

When determining the run order of filters, it sorts them first by their order (lowest numbers first), then by their scope (also lowest numbers first). A sorted example:

  • Order -100, Scope Last
  • Order 0, Scope First
  • Order 0, Scope Global
  • Order 0, Scope Controller
  • Order 0, Scope Action
  • Order 0, Scope Last
  • Order 100, Scope First

It's important to note here that "Controller" scope means "filters applied at the controller level"; the controller itself is also a filter which always runs first (that is, its order is Int32.MinValue and Scope is First). The execution order of filters with the same order and scope is undefined.

When the system runs the filters, they are sometimes run from the front of the list to the back (forwards), and sometimes run from the back of the list to the front (backwards). Additionally, filters may be skipped in some situations.

IActionFilter.OnActionExecuting is run in forward order, and IActionFilter.OnActionExecuted is run in reverse order. If your OnActionExecuting was never called (because an earlier filter terminated the chain), then your OnActionExecuted will not be called either.

IResultFilter.OnResultExecuting and OnResultExecuted follows the exact same rules as IActionFilter.

IAuthorizationFilter.OnAuthorization always runs in forward order, and IExceptionFilter.OnError always runs in reverse order.

In earlier versions of MVC, IExceptionFilter.OnError ran in forward order; for MVC 3, we have reversed the order of this filter based on community feedback. Exception filters in MVC have a similar feel to exception handlers in .NET, which unwind from the inside out. While this reversal is technically a breaking change, we're confident that it now behaves with better predictability.

Implementing a Filter

A filter is a class which implements one or more of the filter interfaces listed above. For filters which are attributes, you can use either the FilterAttribute or the ActionFilterAttribute as a base class. For non-attribute filters, there is no default base class; simply implement the filter interfaces as appropriate.

In addition to supporting one or more of the filter interfaces, you may also choose to implement IMvcFilter:

namespace System.Web.Mvc {
    public interface IMvcFilter {
        bool AllowMultiple { get; }
        int Order { get; }
    }
}

This is a metadata interface which allows you to tell the MVC framework how to treat instances of this filter.

We have updated the FilterAttribute base class to automatically implement this interface for you. The IMvcFilter.Order property mirrors the value of FilterAttribute.Order, and the IMvcFilter.AllowMultiple property is derived automatically based on the [AttributeUsage(AllowMultiple=)] attribute property applied to your custom filter attribute.

You probably noticed above that the Filter object's constructor takes an optional order parameter, which is null by default. By passing null, you're asking the Filter class to look for the implementation of IMvcFilter on the filter object itself to determine its order.

The AllowMultiple property of IMvcFilter is used during sorting in order to eliminate duplicate filters. When AllowMultiple is true, that means that all instances of the exact same filter type are allowed; when AllowMultiple is false, that means that only the last instance of the exact same filter type is allowed, and all others are discarded. If your filter does not implement IMvcFilter, then the default value for AllowMultiple is true.

Previously, all these implementation details were hidden inside the action descriptor classes and filter attribute classes.

Registering a Global Filter

A global filter is a filter that is going to run for every single action on every single controller. You can register a global filter by using the GlobalFilters.Filter static registration endpoint. When you specify a filter instance here, you cannot provide the scope, as it is automatically set to Global for you:

public sealed class GlobalFilterCollection : IEnumerable<Filter>, IFilterProvider {
    public int Count { get; }

    public void Add(object filter, int? order = null);
    public void Clear();
    public bool Contains(object filter);
    public void Remove(object filter);
}

Although you add filter instances as objects to the collection, it actually returns an instance of the Filter metadata class when you enumerate the container.

At this point in time, there is no way to register a global filter with the service locator.

Adding Dependency Injection to Filters

A filter provider is responsible to returning instances of filter objects to the MVC framework, so it's responsible for ensuring that dependency injection is properly observed, whenever possible.

One place where dependency injection has been difficult in the past is inside the filter attributes themselves. Because the .NET framework runtime is actually responsible for creating these attribute instances, we cannot use a traditional dependency injection strategy.

The Unity container is capable of providing non-constructor injection on existing object instances. Here is an example filter provider that overrides the default behavior of the FilterAttributeFilterProvider to enable property setter injection on filter attributes:

UnityFilterAttributeFilterProvider.cs

using System.Collections.Generic;
using System.Web.Mvc;
using Microsoft.Practices.Unity;

public class UnityFilterAttributeFilterProvider : FilterAttributeFilterProvider {
    private IUnityContainer _container;

    public UnityFilterAttributeFilterProvider(IUnityContainer container) {
        _container = container;
    }

    protected override IEnumerable<FilterAttribute> GetControllerAttributes(
                ControllerContext controllerContext,
                ActionDescriptor actionDescriptor) {

        var attributes = base.GetControllerAttributes(controllerContext,
                                                      actionDescriptor);
        foreach (var attribute in attributes) {
            _container.BuildUp(attribute.GetType(), attribute);
        }

        return attributes;
    }

    protected override IEnumerable<FilterAttribute> GetActionAttributes(
                ControllerContext controllerContext,
                ActionDescriptor actionDescriptor) {

        var attributes = base.GetActionAttributes(controllerContext,
                                                  actionDescriptor);
        foreach (var attribute in attributes) {
            _container.BuildUp(attribute.GetType(), attribute);
        }

        return attributes;
    }
}

Global.asax.cs

protected void Application_Start() {
    // ...

    var oldProvider = FilterProviders.Providers.Single(
        f => f is FilterAttributeFilterProvider
    );
    FilterProviders.Providers.Remove(oldProvider);

    var container = new UnityContainer();
    var provider = new UnityFilterAttributeFilterProvider(container);
    FilterProviders.Providers.Add(provider);

    // ...
}

This code removes the old FilterAttributeFilterProvider implementation and replaces it with the one that can use Unity to do property setter injection on the filter attributes, so that you could write attributes like this:

InjectedFilterAttribute.cs

using System;
using System.Web.Mvc;
using Microsoft.Practices.Unity;

public class InjectedFilterAttribute : ActionFilterAttribute {

    [Dependency]
    public IMathService MathService { get; set; }

    public override void OnResultExecuted(ResultExecutedContext filterContext) {
        filterContext.HttpContext.Response.Write(
            String.Format("<p>The filter says 2 + 3 is {0}.</p>",
                          MathService.Add(2, 3))
        );
    }
}

Of course, you can also register the new filter provider in the service locator, instead of using the static registration endpoint:

Global.asax.cs

protected void Application_Start() {
    // ...

    var oldProvider = FilterProviders.Providers.Single(
        f => f is FilterAttributeFilterProvider
    );
    FilterProviders.Providers.Remove(oldProvider);

    var container = new UnityContainer();
    var provider = new UnityFilterAttributeFilterProvider(container);
    container.RegisterInstance<IFilterProvider>(provider, "attributes");
    MvcServiceLocator.SetCurrent(new UnityMvcServiceLocator(container));

    // ...
}

What's Next?

This is the end of the road for MVC 3 Preview 1. In our next preview, we will probably have enabled even more service location scenarios, which we will then be able to document here. In the meantime, if there are specific areas where you wish the framework would better enable service location and/or dependency injection, please don't hesitate to discuss them on the MVC Forums.

Thanks for reading!

posted @ 2011-01-25 17:46  四眼蒙面侠  阅读(550)  评论(0编辑  收藏  举报