Marcin On ASP.NET

Keeping my eye on the dot

November, 2010

Posts
  • Marcin On ASP.NET

    Fixing area view lookups when using multiple view engines

    • 1 Comments

    11/17 Update: I've fixed a bug in the view engine registration code that prevented this method from working. Please note the addition of the dummyFormats property in the solution below.

    Oskar from the ASP.NET forums asked a question about the order of view lookups when using Areas in MVC 3. The problem is that now that MVC ships with two view engines registered by default (WebFormViewEngine and RazorViewEngine), the order of lookups might not work as expected when you are using Areas in conjunction with a mixed environment of Aspx and Razor views.

    Take the following application layout as an example. This application has an “Admin” area with a Home controller and a Home Index view written in Razor. There’s also a default Home Index view written using the Aspx view engine.

    AreaLookup

    The Problem

    When requesting the Index action of the Admin area’s HomeController you would expect Index.cshtml to be served. However, in MVC 3 you will actually be served Index.aspx. This is because the process for looking up a View involves querying the collection of registered view engine until the first successful match. Once a successful match is found the remaining view engines are ignored. This gets a bit counter-intuitive when you mix Razor and Aspx views because this is the effective search order:

    ~/Areas/Admin/Views/Home/Index.aspx
    ~/Areas/Admin/Views/Home/Index.ascx
    ~/Areas/Admin/Views/Shared/Index.aspx
    ~/Areas/Admin/Views/Shared/Index.ascx
    ~/Views/Home/Index.aspx
    ~/Views/Home/Index.ascx
    ~/Views/Shared/Index.aspx
    ~/Views/Shared/Index.ascx
    ~/Areas/Admin/Views/Home/Index.cshtml
    ~/Areas/Admin/Views/Home/Index.vbhtml
    ~/Areas/Admin/Views/Shared/Index.cshtml
    ~/Areas/Admin/Views/Shared/Index.vbhtml
    ~/Views/Home/Index.cshtml
    ~/Views/Home/Index.vbhtml
    ~/Views/Shared/Index.cshtml
    ~/Views/Shared/Index.vbhtml

    Notice that because the WebFormsViewEngine is registered first by default, the path Views/Home/Index.aspx file gets looked at before the Areas/Admin/Views/Home/Index.cshtml path. That’s because the MVC Areas support is not hardcoded as a general pattern. Instead, it’s the job of each view engine to implement the lookup ordering and fallback logic.

    To make things work intuitively, you would need a multi-pass system that first scans all view engines for area-specific matches and then does another pass asking for general matches. But that is not how things are implemented in MVC (mainly because Areas did not exist in MVC 1 and it was not possible to change the existing view lookup contracts).

    The Solution

    However, it’s actually quite easy to reconfigure MVC to emulate this multi-pass behavior. Just use multiple instances of view engines. In your Global.asax.cs file add the following code (don’t forget to call this method from Application_Start):

    public static void SetUpViewEngines(ViewEngineCollection viewEngines) {
        viewEngines.Clear();
    
        string[] dummyFormats = new[] { "~/ThisFileShouldNotExist.aspx" };
    
        // area-specific pass
        viewEngines.Add(new WebFormViewEngine {
            ViewLocationFormats = dummyFormats,
            PartialViewLocationFormats = dummyFormats,
            MasterLocationFormats = dummyFormats
        });
        viewEngines.Add(new RazorViewEngine {
            ViewLocationFormats = dummyFormats,
            PartialViewLocationFormats = dummyFormats,
            MasterLocationFormats = dummyFormats
        });
    
        // general pass
        viewEngines.Add(new WebFormViewEngine {
            AreaViewLocationFormats = null,
            AreaPartialViewLocationFormats = null,
            AreaMasterLocationFormats = null
        });
        viewEngines.Add(new RazorViewEngine {
            AreaViewLocationFormats = null,
            AreaPartialViewLocationFormats = null,
            AreaMasterLocationFormats = null
        });
    }

    This solution works by first registering a pair of WebForms and Razor view engines that have dummy general location formats pointing to a file that does not exist (meaning that they are effectively only aware of the area-specific locations formats) and then a pair that only knows about the general location formats. This way both Aspx and Razor views will be searched for in area locations and if neither succeed the general views will be searched.

    This technique should be helpful for those who want to transition their application from Aspx to Razor (or maybe between some other pair of view engines). However, once you are done with the transition you should probably stick to just having one view engine registered to avoid unnecessary searches in the other view engines.

  • Marcin On ASP.NET

    Granular Request Validation in ASP.NET MVC 3

    • 7 Comments

    12/10 Update: In MVC 3 RC 2 SkipRequestValidationAttribute got renamed to AllowHtmlAttribute. I have updated the examples below.

    A little while ago I wrote a blog post describing granular request validation that shipped in MVC 3 Beta. However, since then we have changed the API for this feature and that post is no longer valid. In this post I will present the new API which is usable in the recently-shipped MVC 3 Release Candidate.

    But first: a quick refresh on request validation and why it’s great to make it granular. Request validation is a feature of ASP.NET that analyzes the data that a browser sends to the server when a user interacts with your site (such as form or query string data) and rejects requests that contain suspicious input that looks like html code (basically anything with a ‘<’). This protects you from HTML injection attacks such as cross-site scripting (XSS). It is enabled by default, however in previous versions it was an all-on-or-off feature, meaning that if you want to be able to accept HTML-formatted input from your users in just one field you had to completely turn this protection off. This in turn meant that you now had to validate every bit of data that came from the client.

    AllowHtmlAttribute SkipRequestValidationAttribute

    In MVC 3 we are introducing a new attribute called AllowHtmlAttribute. You can use this attribute to annotate your model properties to indicate that values corresponding to them should not be validated. Let’s take this User model and UserController as an example:

    public class User {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        [AllowHtml]
        public string Description { get; set; }
        [AllowHtml]
        public string Bio { get; set; }
    }
    
    public class UserController {
        [HttpPost]
        public ActionResult Update(User user) {
            // update user database
        }
    }

    I have annotated the Description and Bio properties to indicate they should not be request-validated. Now when the Update action method gets invoked these two properties on the User object will not be validated and any HTML they might contain will be passed straight through to the action method. However, everything else will still go through request validation and requests that contain suspicious content in the Name or Email fields will get rejected.

    ValidateInputAttribute

    AllowHtmlAttribute can only be applied to properties of a model class. For other request validation scenarios the existing ValidateInputAttribute is still helpful. For example, you can use it to disable request validation for action methods that bind to a loose collection of parameters:

    [ValidateInput(false)]
    public ActionResult Update(int userId, string description) {
    }

    Now when the parameters of the Update method get bound request validation will not be performed. You can apply ValidateInput to action methods as shown above or to the entire controller to affect all of its action methods.

    ValidateInput is also more usable in MVC 3. In MVC 2 running on .NET 4 you had to set requestValidationMode="2.0" in order to turn request validation off. In MVC 3 this is no longer necessary.

Page 1 of 1 (2 items)