Yan-Feng

记录经历、收藏经典、分享经验

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Overview

Chapter 5 covered Controllers and Controller actions, which are responsible for coordinating interactions between the user, the model, and the view. A given action method typically handles a specific user interaction with the view.

For example, when a user clicks a link to display all products on a page, that click might create a request to an action method of ProductsController named List, which reaches into the domain objects, grabs all the products, slaps them together with the view, and displays the result to the user.

Grabbing the products data and selecting the view is the primary responsibility of that action method. It's not responsible for cross-cutting concerns such as logging, output caching, authentication, and authorization. These are responsibilities that your action method shouldn't need to know about. In mathematical parlance, they are orthogonal to the purposes of your action method.

This is where filters come into play. Filters are a declarative attribute based means of providing cross-cutting behavior to an action method.

 

Filters Included with ASP.NET MVC

Before you delve into how one goes about writing a filter, let's take a look at the filters that are included with ASP.NET MVC.

ASP.NET MVC includes the following three action filters out of the box:

  • Authorize: This filter is used to restrict access to a Controller or Controller action.

  • HandleError: This filter is used to specify an action that will handle an exception that is thrown from inside an action method.

  • OutputCache: This filter is used to provide output caching for action methods.

Let's take a look at each of these filters in more depth.

Authorize

The AuthorizeAttribute is the default authorization filter included with ASP.NET MVC. Use it to restrict access to an action method. Applying this attribute to a Controller is shorthand for applying it to every action method.

Keep the following in mind when applying this filter:

  • When applying this attribute, you can specify a comma delimited list of Roles or Users. If you specify a list of Roles, the user must be a member of at least one of those roles in order for the action method to execute. Likewise, if you specify a list of Users, the current user's name must be in that list.

  • If you don't specify any roles or users, then the current user must simply be authenticated in order to call the action method. This is an easy way to block unauthenticated users from a particular Controller action.

  • If a user attempts to access an action method with this attribute applied and fails the authorization check, the filter causes the server to return a "401 Unauthorized" HTTP status code.

  • In the case that forms authentication is enabled and a login URL is specified in the web.config, ASP.NET will handle this response code and redirect the user to the login page. This is an existing behavior of ASP.NET and is not new to ASP.NET MVC.

Important 

Product Team Aside

Originally, we looked at the PrincipalPermissionAttribute as a potential solution to securing a Controller action, but we ran into a couple of issues. First of all, when applied to a class, the PrincipalPermissionAttribute would cause an exception when trying to even instantiate the Controller class if the security check failed. This was not our desired behavior as we may want other filters to run even if the security check fails, such as a logging filter.

Second, we wanted to control the status code that was raised. The PrincipalPermissionAttribute simply throws a SecurityException.

Let's look at a simple example of usage. In the following code, you have an admin Controller that is restricted to members of the Admins and SuperAdmins role. Notice that the roles are comma delimited. The filter will trim off spaces in between commas to allow for improved readability when applying the attribute.

[Authorize(Roles="Admins, SuperAdmins")]public class AdminController{    //Only admins should see this.    public ActionResult Index()    {      return View();    }    //Only admins should see this.    public ActionResult DeleteAllUsers()    {        //Thankfully, this is secured by the Authorize attribute.    }}

After thinking about the last example, a bit, the authors realized that we don't want any admin or superadmin to be able to call the DeleteAllUsers action. Really, we only trust Phil to do this, so we apply a more specific authorization to it which only allows the user Phil to call it. When multipl authorization filters are applied, the user must satisfy all authorization filters to call the action. Thus in this case, Phil must also be a member of the Admins role or the SuperAdmins role.

Note 

One thing to note, in general: it makes more sense to use roles than specific user names even in a situation like this. A better approach might be to create a role named CanDeleteAllUsers and add Phil to that role and then apply an authorization filter that specifies that role.

 [Authorize(Roles="Admins, SuperAdmins")]public class AdminController{    //Only admins should see this.    public ActionResult Index()    {      return View();    }    //Only Phil should do this.    [Authorize(Users="Phil")]    public ActionResult DeleteAllUsers()    {        //...    }}

In this final example, you switch over to a Controller that provides user management for individual users; you simply don't specify User and Roles. In this case, the Controller might be open to anyone who is authenticated.

 [Authorize]public class UsersController{    public ActionResult ManageProfile()    {        //...        return View();    }}

OutputCache

The OutputCacheAttribute is used to cache the output of an action method. This attribute is a hook into the built-in ASP.NET output cache feature and provides largely the same API and behavior that you get when using the @OutputCache page directive. There are a couple of minor differences discussed later.

Because output caching is a well-known feature of ASP.NET, this section doesn't delve too much into the specifics of the caching behavior itself, but rather concentrates on the MVC implementation. One question that might be on your mind is "why not simply use the @OutputCache directive in my view?"

With MVC, the Controller action executes first before a view is even selected. Thus putting output cache on a view will actually work to cache the output of that view, the action method itself will still execute on every request, thus negating most of the benefits of caching the output.

By applying this attribute to an action method, the filter can then determine whether the cache is valid and skip calling the action method and go right to rendering the contents of the cache.

API

The following table lists the properties of the OutputCacheAttribute class. These are all settings used to control how the OutputCache filter performs its caching.

Open table as spreadsheet

Property

Description

CacheProfile

The name of the cache settings to use. This allows placing cache configuration in the web.config file rather than in the attribute. The attribute can then reference the config settings via this property.

Duration

Specifies the number of seconds the output is stored in the cache.

Location

Specifies where the content may be cached. The enumeration OutputCacheLocation contains the allowed locations: Any, Client, Downstream, Server, None, ServerAndClient.

NoStore

Sets the "Cache-Control: Private, no-store" HTTP header to prevent the browser from caching the response. Equivalent to calling Response.Cache.SetNoStore.

SqlDependency

A specially formatted string value containing a set of database and table name pairs that the output cache depends on. When the data in these tables changes, the cache is invalidated.

VaryByContentEncoding

Introduced in ASP.NET 3.5, this is a comma-delimited list of content encodings used to vary the cache by.

VaryByCustom

Determines whether to cause a new version of the output to be cached based on a call to GetVaryByCustomString within the Global.asax.cs file. This gives the developer full control over when to cache.

VaryByHeader

Varies the cache based on http header. For example, you may use this to cache different versions of the output based on the Accept-Language header.

VaryByParam

Used to specify which QueryString parameters cause a new version of the output to be cached.

Difference with the @OutputCache Directive

Because of the differences between the ASP.NET MVC and Web Forms model of app development, there is one option that doesn't translate over to the output cache filter, the VaryByControl property. Hence this property is missing from the OutputCacheAttribute.

Usage Examples

Let's look at some common usage patterns for the output cache filter. In many cases, you may want to simply cache the output of an action for a short duration. In the example code below, you cache the output of the default About action for 60 seconds. You changed the implementation of the method to display the time.

[OutputCache(Duration=60, VaryByParam="none")]public ActionResult About(){    ViewData["Title"] = "This was cached at "+ DateTime.Now;    return View();}

In thinking through the previous example, you don't want to have to recompile your code every time you want to change the duration. Instead, you might like to set the duration in a configuration file.

Fortunately, there is already an output cache settings section within web.config. The sample that follows demonstrates how you might add cache settings to web.config:

<system.web>  <caching>    <outputCacheSettings>      <outputCacheProfiles>        <add name="MyProfile" duration="60" varyByParam="none" />      </outputCacheProfiles>    </outputCacheSettings>  </caching></system.web>

Note that you added a cache profile named "MyProfile". Now you can change your output cache filter to read in the settings for that profile via the CacheProfile property:

[OutputCache(CacheProfile="MyProfile")]public ActionResult About(){    ViewData["Title"] = "This was cached at "+ DateTime.Now;    return View();}

Exception Filter

The HandleErrorAttribute is the default exception filter included with ASP.NET MVC. Use it to specify an exception type to handle and a view (and master view if necessary) to display if an action method throws an unhandled exception that matches or is derived from the specified exception type.

By default, if no exception type is specified, then the filter handles all exceptions. If no view is specified, the filter defaults to a view named "Error". The default ASP.NET MVC project includes a view named "Error.aspx" within the Shared folder.

Let's look at a quick example:

[HandleError(ExceptionType = typeof(ArgumentException), View="ArgError")]public ActionResult GetProduct(string name){  if(name == null)  {    throw new ArgumentNullException("name");  }  return View();}

Because ArgumentNullException derives from ArgumentException, passing null to this action method will result in the ArgError view being displayed.

In some cases, you may want to have multiple exception filters applied to the same action method. It is important to specify an ordering in these cases in order to place the most specific exception types first and the least specific to the end. For example, in the following code snippet:

//This is WRONG![HandleError(Order=1, ExceptionType=typeof(Exception)][HandleError(Order=2, ExceptionType=typeof(ArgumentException), View="ArgError")]public ActionResult GetProduct(string name){  ...}

the first filter is more generic than the ones after it and will handle all exceptions, never giving the second filter an opportunity to handle the exception. To fix this, simply order the filters from most specific to least specific.

//This is BETTER![HandleError(Order=1, ExceptionType=typeof(ArgumentException), View="ArgError")[HandleError(Order=2, ExceptionType=typeof(Exception)]public ActionResult GetProduct(string name){  ...}

When this exception filter handles an exception, it creates an instance of the HandleErrorInfo class and sets the Model property of the ViewDataDictionary instance when rendering the Error view.

The following table shows the HandleErrorInfo class properties:

Open table as spreadsheet

Property

Description

Action

The name of the action that threw the exception

Controller

The name of the Controller in which the exception was thrown

Exception

The exception that was thrown

Note that this filter doesn't catch exceptions in debug builds when custom errors are not enabled. The filter simply checks the value of HttpContext.IsCustomErrorEnabled to determine whether to handle the exception. The reason for this is to allow the more informative "Yellow Screen of Death" to display information about the exception during the development phase.

Custom Filters

The preceding section covered three filters included with ASP.NET MVC. Each of these is an example of a different filter type. The AuthorizeAttribute is an example of an authorization filter, the OutputCacheAttribute is an example of an action/result filter, and the HandleError is an example of an exception filter.

Each of these filter types is embodied by an interface. More specifically, implementing a filter attribute requires writing a class that inherits from FilterAttribute and implements one of the four filter interfaces:

  • IAuthorizationFilter

  • IActionFilter

  • IResultFilter

  • IExceptionFilter

Action and Result filters are the most common types of filters. If you find yourself needing to hook in some custom logic before and after an action method is executed, or before and after an action result is executed, these are the two types of filters you might implement.

They are so common, in fact, that the MVC team included a base class that inherits from FilterAttribute and implements both interfaces, ActionFilterAttribute. Thus when writing an action filter or a result filter (or a combination of the two), it is easier to simply inherit from ActionFilterAttribute.

Authorization and Exception filters exist for specific scenarios needed by the framework itself. In most cases, you will not need to write an authorization filter nor an exception filter. If you do, make sure that you understand the implications. While these were added to support the framework internally, they've been made available to the application developer and come with a big warning sign.

Important 

Product Team Aside

Early on during the development of ASP.NET MVC, there was only one filter type, action filters. The development team ran into scenarios in which action filters were not sufficient when combined with each other. For example, when using both an output cache filter and an authorization filter, it is very important that the authorization filter always run first. While it was possible for the developer using these filters to specify the order, that the filters ran, it was easy to get this wrong.

For example, if a developer has her own base "admin" Controller class with an authorization filter applied, and tries to apply an output cache filter to an action method; the developer might get the ordering wrong by accident. The same problems exist with exception filters; the exception filter might not run late enough to catch an exception thrown by another filter.

To alleviate these problems, the ASP.NET MVC team decided to add two more filter types — one for authorization and one for exception handling. In most cases, the team does not expect developers to need to implement their own authorization filters nor exception filters very often, if at all, other than to perhaps inherit from the existing ones for specialized filters (for example, an AdminOnlyAttribute might simply inherit from AuthorizeAttribute but set the role to "Administrators").

For most scenarios, an action filter is the most appropriate type of filter to write. Writing custom authorization filters is an advanced scenario and should only be undertaken if you fully understand the consequence of doing so. For example, you should not write an authorization filter that assumes that the default authorize filter has already run, since it would be possible to place your custom filter before the AuthorizeAttribute filter.

ActionFilterAttribute

As mentioned in the previous section, the easiest way to develop a custom action filter is to have it inherit from ActionFilterAttribute. This attribute has the following four methods you can choose to override:

public virtual void OnActionExecuted(ActionExecutedContext filterContext);public virtual void OnActionExecuting(ActionExecutingContext filterContext);public virtual void OnResultExecuted(ResultExecutedContext filterContext);public virtual void OnResultExecuting(ResultExecutingContext filterContext);

The first two methods have the same signature as the IActionFilter interface, while the second two methods come from the IResultFilter interface.

OnActionExecuting is called after the action method has been located by the action invoker, but before the action method is called. At this point, the Controller context is available to the action method and the TempData dictionary has been populated with any temp data there might be.

After the action method is called, but before the action result is executed, the invoker calls OnActionExecuted. Because all action methods return an action result, your filter can also be called before and after the result is executed via the OnResultExecuting and OnResultExecuted methods, respectively.

Action Filter Contexts

Each method of an action filter is provided a specific context object. These context objects provide information to the filter and in some cases allow the filter to cancel an action.

ActionExecutingContext

The ActionExecutingContext allows an action filter to cancel an action. If the Cancel property is set to true, then the OnActionExecuting method of any filters further down the filter stack will not be called and the invoker starts calling the OnActionExecuted method for filters that already had their OnActionExecuting method called.

That last sentence might be a bit confusing because of the similarity of the methods' names, but the basic idea is that if your action filter has its OnActionExecuting method called and that method doesn't set Cancel to true and returns (in other words, it doesn't throw an exception), your filter is guaranteed to have its OnActionExecuted method called even when an action method later on cancels the event.

Canceling the action method allows an action filter to "short-circuit" the call to the action method. For example, if the AuthorizeAttribute determines that the current request does not have access to the current action method, it cancels the action so that the method is not called, and sets the Result property of the filter context to an action result that sets the status code to 401 Unauthorized Access.

The following table shows the ActionExecutingContext class properties.

Open table as spreadsheet

Property

Description

ActionParameters

A dictionary of parameters that will be passed to the action method.

Result

When canceling an action method, a filter can provide its own action result to use instead of the one that would have been returned by the action method.

ActionExecutedContext

The ActionExecutedContext is supplied to an action filter after the action method is called. At this point, it is too late to cancel an action method, but you can query the context to find out if the action method was canceled via the Canceled property.

If an exception was thrown by the action method or by another action filter, the Exception property will be set to the thrown exception. If the exception is not handled, the action result will not be executed by the action invoker. To handle the exception, your action filter can set the ExceptionHandled property to true. In general, action filters should never do this unless they are absolutely sure that they can handle the exception properly.

By setting the ExceptionHandled property, your filter indicates to the invoker that it should go ahead and execute the action result. Keep in mind, though, that the OnActionExecuted method of other action filters further down the stack are still called. Thus, if an action filter higher up the stack handles the exception, your filter will still be able to examine the exception in case it needs to take a different behavior.

Note 

When an exception is thrown, we lose the result that is winding its way back up the call stack. So if you have two action filters — A and B — where A executes before B, and if the action method returns some FooResult, and if B.OnActionExecuted throws an exception (eek!), then A.OnActionExecuted will never see the FooResult originally returned by the action method. It is lost forever.

The ActionExecutedContext also contains a Result property, which allows you to modify or replace the action result returned by the action method.

The following table lists properties of the ActionExecutedContext.

Open table as spreadsheet

Property

Description

Canceled

Indicates whether or not another filter canceled the action.

Exception

If an exception was thrown before the current filter was called, this property contains that exception.

ExceptionHandled

Setting this to true indicates to the action invoker (and other action filters) that the exception has been handled and that the result may be executed.

Result

The action result returned by the action method (or another action filter). The filter can modify or replace this result.

ResultExecutingContext

The ResultExecutingContext allows a filter to cancel the call to execute the action result. If the Cancel property is set to true, then the OnResultExecuting method of any filters further down the filter stack will not be called and the invoker starts calling the OnResultExecuted method for filters who already had their OnResultExecuting method called, much like the behavior for setting Cancel with the ActionExecutingContext.

Note that if you set Cancel to true, the result will not be executed. In some cases, this makes sense if you also issue an HTTP redirect, for example. Otherwise, the contents of the response may end up being blank.

The following table lists the properties of the ResultExecutingContext class.

Open table as spreadsheet

Property

Description

Cancel

Setting this to true cancels the call to the action result.

Result

A result filter can set this property to provide its own action result to use instead of the one that was returned by the action method.

ResultExecutedContext

The ResultExecutedContext is supplied to a result filter after the action result has been executed and has the same properties as the ActionExecutedContext.

If an exception was thrown by the action result, the Exception property will be set to the thrown exception. If the exception is not handled by any result filter, the exception will be rethrown by the invoker. To handle the exception, your filter can set the ExceptionHandled property to true.

By setting the ExceptionHandled property, your filter indicates to the invoker that it should not rethrow the exception. If the exception was thrown during rendering the response, then handling the exception could mean that the user sees a partially rendered view with nothing to indicate that anything went wrong.

If the exception is not handled, then the exception might still be handled by an exception filter, or propagate all the way up to normal ASP.NET handling of uncaught exceptions.

The ResultExecutedContext also contains a Result property, which allows you to examine the action result returned by the action method.

The following table lists properties of the ResultExecutedContext.

Open table as spreadsheet

Property

Description

Canceled

Indicates whether or not another filter canceled the action.

Exception

If an exception was thrown before the current filter was called, this property contains that exception.

ExceptionHandled

Setting this to true indicates to the action invoker that the exception has been handled and should not be rethrown.

Result

The action result returned by the action method (or another action filter). The filter can examine this result, but not replace it.

 

Writing a Custom Action Filter

Let's put all this knowledge about action filters to good use and write one. The authors start off with a simple example of an action filter you can use to time the duration of an action method, not including the time spent executing its action result.

using System.Diagnostics;using System.Web.Mvc;public class TimerAttribute : ActionFilterAttribute{    public TimerAttribute()    {        //By default, we should be the last filter to run        //so we run just before and after the action method.        this.Order = int.MaxValue;    }    public override void OnActionExecuting(ActionExecutingContext filterContext)    {        var controller = filterContext.Controller;        if (controller != null)        {           var stopwatch = new Stopwatch();           controller.ViewData["__StopWatch"] = stopwatch;           stopwatch.Start();       }    }    public override void OnActionExecuted(ActionExecutedContext filterContext)    {        var controller = filterContext.Controller;        if (controller != null)        {            var stopwatch = (Stopwatch)controller.ViewData["__StopWatch"];            stopwatch.Stop();            controller.ViewData["__Duration"] =     stopwatch.Elapsed.TotalMilliseconds;        }    }}

This filter simply adds and starts an instance of the Stopwatch class to the ViewData before the action method is called. After the action method is called, the filter retrieves the stopwatch instance and adds the elapsed time to the ViewData dictionary.

You can now apply this new action filter to an action method. In this example, the action method sleeps for a random number of milliseconds in order to make this demonstration more interesting.

[Timer]public ActionResult Index() {    ViewData["Title"] = "Home Page";    ViewData["Message"] = "Welcome to ASP.NET MVC!";    var rnd = new Random();    int randomNumber = rnd.Next(200);    Thread.Sleep(randomNumber);    return View();}

Every time that Index is called, the timer filter adds the elapsed time to the view data. The next step is to change the view for this action method to display the elapsed time by adding the following snippet to Index.aspx.

<p>    The duration was: <%= ViewData["__Duration"] %></p>

The sample code included with this book includes a demonstration of this custom action filter.

 

Writing a Custom Authorization Filter

The AuthorizeAttribute filter included with ASP.NET MVC is not intended to be a base filter for all authorization filters. Unlike action filters, there is no default base type for authorization filters. It is simply a concrete default implementation. Not including a base authorization filter type was a conscious design decision by the team to highlight that writing filters of this type is considered a more advanced scenario. For example, ensuring that the authorization filter works in combination with an OutputCache filter is tricky.

To write an authorization filter, write a class that inherits FilterAttribute and implements the IAuthorizationFilter interface. For example, in the following code listing, you have the full code for a very simple authorization filter. This filter uses ASP.NET's built-in request validation code when applied to an action method or Controller to validate incoming requests.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,  Inherited = true, AllowMultiple = false)]public sealed class ValidateInputAttribute : FilterAttribute    , IAuthorizationFilter {  public void OnAuthorization(AuthorizationContext filterContext) {    filterContext.HttpContext.Request.ValidateInput();  }}

The OnAuthorization method is given an AuthorizationContext instance, which has an API that is very similar to the ResultExecutingContext.

The following table lists details about the single property of the AuthorizationContext class.

Open table as spreadsheet

Property

Description

Cancel

Setting this to true cancels the call to the action result.

Result

If Cancel is set to true, an authorization filter can set this property to provide its own action result to use.

Because authorization filters run before any of the action filters, setting Result to some non-null value will immediately bypass any remaining filters. Execution jumps to calling ExecuteResult on the action result set in the Result property. When setting Cancel to true, make sure to set the Result property to an ActionResult instance that you want executed.

For instance, let's look at a silly authorization filter purely for demo purposes. This filter will block access to all requests and replace the action result with a string to render to the response.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,  Inherited = true, AllowMultiple = false)]public class NoAuthAttribute : FilterAttribute, IAuthorizationFilter{public void OnAuthorization(AuthorizationContext filterContext)  {    filterContext.Result = new ContentResult    { Content = "You've been blocked by the NoAuth filter." };  }}

If you apply this to the About action, for example, and try to visit the about page, you'll see Figure 8-1 instead.

Image from book
Figure 8-1
 

Writing a Custom Exception Filter

Exception filters run after all other filters have run and give you an opportunity to handle any exceptions that may have been thrown. When an exception is thrown, all exception filters get a chance to look at the exception, even if the exception is already handled. This makes it possible, for example, to write a logging filter by implementing an exception filter. Like authorization filters, there is no common base type for exception filters.

To write an exception filter, write a class that inherits FilterAttribute and implements the IExceptionFilter interface. For example, in the following code listing, you have the full code for a very simple exception filter used to log exceptions:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true,   AllowMultiple = false)]public class LogItAttribute : FilterAttribute, IExceptionFilter{  public void OnException(ExceptionContext filterContext)  {    var trace = filterContext.HttpContext.Trace;    trace.Write("Action "+ (string)filterContext.RouteData.Values["action"] +      "called.", "Message");    if (filterContext.Exception != null)    {      trace.Write(filterContext.Exception.Message, "Exception!!!");    }  }}

After applying this to an action method, you can visit /Trace.axd (assuming that you have tracing enabled via <trace enabled="true" /> within the system.web section of web.config) to see the trace messages created by this filter.

The OnException method is given an ExceptionContext instance, which provides information about the exception that was thrown, if any, and allows the filter to replace the result with one of its own.

The following table lists properties of the ExceptionContext.

Open table as spreadsheet

Property

Description

Exception

The exception that was thrown, if any. Null if there was no exception.

ExceptionHandled

A filter can set this to true to indicate that it has handled the exception.

Result

An exception filter can provide an action result to display in the case that an exception was thrown and a result was not rendered to the view.

 
 

Filter Ordering

As you might expect, authorization filters run before action filters, which run before result filters, which run before exception filters. Within these groups, it would be ideal if the order in which filters of the same filter type execute didn't matter. For example, it would be ideal if all action filters could be run in arbitrary order.

Note 

By "filter type" here we don't mean CLR type. Rather, we are referring to action filter vs. authorization filter, and so on, rather than the actual type of the attribute. Thus, it would be nice if all action filters (regardless of CLR type) could be run in arbitrary order.

Ideal, but not realistic.

There may be many reasons that you need filters to run in a specific order. For example, exception filters need to have the most specific filters run before the least specific filters. That's where filter ordering comes in. The FilterAttribute (which all filters derive from) contains an Order property used to specify the order that each filter should be executed in.

The following four rules defines how filters are ordered:

  1. A filter with an Order value strictly less than another filter will execute before that filter. Thus a filter with Order = 1 will always execute before a filter with Order = 2.

  2. If two filters have the same Order value, but one is defined on an action method and the other is defined on the Controller, then the one defined on the Controller will go first. For example, in the following code snippet:

    [FilterA(Order=1)]public class MyController : Controller{  [FilterB(Order=1)]  public ActionResult SomeAction()  {    //...  }}

    FilterA will run before FilterB even though they have the same Order.

  3. Any filter that does not have an order explicitly provided has a default order of negative one.

  4. If the target Controller itself implements a filter interface (IActionFilter, etc.), that method will be called before any other filter of that type. For example, in the following code snippet:

    [MyActionFilter]public class MyController : Controller{  protected override OnActionExecuting(ActionExecutingContext context)  {    //...  }}

    The method OnActionExecuting of the Controller will run before the same method of MyActionFilter does.

  5. Filters of the same type, which have the exact same order according to the rules above, run in a nondeterministic order. For example, given the following action method:

    [MyActionFilterOne][MyActionFilterTwo][MyActionFilterThree]public ActionResult SomeActionMethod(){}

It is possible that in one request, MyActionFilterOne executes last, while in a subsequent request, it executes first. Because the order of the three attributes are exactly the same, they are of the same type, and they are defined on the same scope, the order of the three is undetermined.

 
 

Filter Naming

One of the most challenging parts of building a framework is naming, and naming filters is no exception. When the ASP.NET MVC team was going through the process of naming the default filter attributes, we noticed that attributes in the larger framework tend to be nouns that describe the method, class, and the like that the attribute is applied to. For example, look at the following code:

[Serializable]public class Product{}[WebService]public class ProductService{  [WebMethod]  public IList<Product> GetProducts()  {    ...  }}

When you see the [Serializable] attribute applied to the Product class, the thought that goes through your mind is that the attribute is describing the Product class and telling you that it is serializable. Likewise, the [WebMethod] attribute is also descriptive. It tells you that GetProducts is a web service method.

However, this rule of thumb doesn't really apply to filter attributes. For example, an early name for the HandleErrorAttribute the team bounced around was ExceptionHandlerAttribute. If the team had taken that approach, you might see a Controller action that looked like this:

public class MyController : Controller{  [ExceptionHandler]  public ActionResult DoSomething()  {    //...  }}

When reading this code, it would be natural to assume that DoSomething is an exception handler method, which would not have been the case. To help remedy this situation, the team came up with the general guideline that filters should be named using a verb that describes the behavior that attribute will take on your behalf, rather than a noun. For example, the HandleErrorAttribute will "handle an error" thrown by an action method. The AuthorizeAttribute will "authorize" the current user to call an action. The OutputCacheAttribute is convenient in that it is both a noun and verb. The action it takes on your behalf is to "cache the output." The team recommends this general naming pattern for filters for consistency.

For example, when you see an action method with several filters like this:

[HandleError][OutputCache]public ActionResult DoSomething(){  //...}

one way you can read the code in your head is "When the DoSomething action is called, the framework will handle an error and cache the output on my behalf," rather than reading this as "DoSomething is an error handler and output cacher."

Another thing to keep in mind with filters is that applying a filter to the Controller is merely shorthand for applying the filter to each action method. There is no Controller-specific lifecycle when applying Controllers. Thus the following:

[HandleError(Order=1)][OutputCache(Order=2)]public class MyController : Controller{  public ActionResult Action1() {...}  public ActionResult Action2() {...}}

is equivalent to:

public class MyController : Controller{  [HandleError(Order=1)]  [OutputCache(Order=2)]  public ActionResult Action1() {...}  [HandleError(Order=1)]  [OutputCache(Order=2)]  public ActionResult Action2() {...}}
 

Summary

Filters provide a powerful mechanism for providing reusable cross-cutting behaviors for action methods. While there are only four types of filters (authorization, action, result, and exception), these four types provide the basis for nearly any sort of additive behaviors you may need for an action method.

While the ASP.NET MVC team hopes that its core set of filters will be enough for a large number of scenarios, they also are keenly aware that there may be a multitude of scenarios they could never anticipate where filters will be very useful. By providing this extensibility layer, the goal is that anything you need to accomplish with a filter is accomplishable. Happy filtering!

 
posted on 2010-06-26 13:29  Yan-Feng  阅读(599)  评论(0编辑  收藏  举报