Long time since the last post!

It’s been a while since my last post. I am concentrating a lot on Windows Phone programming, so my future posts might cover more Windows Phone platform and less about ASP.NET. There will be a new Windows Phone blog that I intend to write where there will be a lot of talk about Line of Business (LOB) applications on Windows 8 and Windows Phone 8. As soon as it is up and running this post will be updated.

Until then, enjoy!

Posted in Uncategorized

Problem targeting ES5 with “HTML5 Application with TypeScript” project.

If you create HTML 5 Application with TypeScript in Visual Studio 2012 and attempt to use new get and set accessors you will get a compiler error:

Accessors are only when targeting EcmaScript5 and higher.

(The missing word in compiler error is reported to the TypeScript team)
I went through Web Essentials options and changed ES3 generation from false to true and back again, but still couldn’t solve it. I thought this might be a bug with Web Essentials. Well, at least I thought that Web Essentials should override the default project setting.

Adding Pre-Build events in Visual Studio project did not help. At the end, the way to solve it is to:

1. Right-click on project name and choose Edit Project file
2. Locate the following elements:

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
    <TypeScriptTarget>ES5</TypeScriptTarget>
    <TypeScriptIncludeComments>true</TypeScriptIncludeComments>
    <TypeScriptSourceMap>true</TypeScriptSourceMap>
    <TypeScriptModuleKind>AMD</TypeScriptModuleKind>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <TypeScriptTarget>ES5</TypeScriptTarget>
    <TypeScriptIncludeComments>false</TypeScriptIncludeComments>
    <TypeScriptSourceMap>false</TypeScriptSourceMap>
    <TypeScriptModuleKind>AMD</TypeScriptModuleKind>
  </PropertyGroup>

and replace the value between <TypeScriptTarget> element from ES3 to ES5.

Save your project file and reload it. The red squiggly lines should disappear from get/set and compilation will pass successfully.

Posted in Development, HTML5, TypeScript | Tagged , , , , , ,

Preventing (or Minimizing) Yellow Screen of Death in ASP.NET MVC

One of the most annoying error screens in ASP.NET is the yellow screen of death. It can be caused by unhandled exceptions in your code, by making requests to resources that do not exist, etc. Although there are great tools for logging these unhandled exceptions, it would be very nice to prevent the YSOD as much as possible. The focus of this article will be on preventing requests to non existing resources in MVC. There are three steps that need to be handled here.

For this article you can create a blank MVC project. I expect that you know how to this. Once the project is created and ready you will notice that there are no controllers in the Controllers folder. Now add two controllers, HomeController and ProductController. The following are the C# codes for the two controllers:

The HomeController.cs:

public class HomeController : Controller
{
    //
    // GET: /Home/
    public ActionResult Index()
    {
        return View("_Common");
    }

    public ViewResult About()
    {
        return View("_Common");
    }
}

The ProductController.cs file:

public class ProductController : Controller
{
    //
    // GET: /Product/

    public ActionResult Index()
    {
        return View("_Common");
    }

    public ActionResult List()
    {
        return View("_Common");
    }
}

As you may have noticed all four actions in the two controllers return a view called _Common. For this view you will need to create a Shared subfolder in your Views folder. Now add a view called _Common.cshtml and make sure that you have the following inside:

@{
    ViewBag.Title = "_Common";
    var routeData = ViewContext.RouteData;
}
<h2>Welcome</h2>
<div>
<div>Controller: @routeData.Values["controller"]</div>
<div>Action: @routeData.Values["action"]</div>

<hr />

<div>@DateTime.Now.ToString()</div>
</div>

With this setup we have a basic MVC application that contains two controllers, Home and Product. Each controller contains two actions. If you start your application and make a request for any of the following:

  • Home/Index
  • Home/About
  • Product/Index
  • Product/List

You will get the same response that looks like the screen below:

So far so good. The application behaves normally and it is returning us the information from the routing system. But what if a user makes a request for an action in Home controller that does not exist. If you try Home/Whatever, you will see an HTTP 404 message with an error stating that a resource cannot be found. OK, this is not a YSOD, but still it is an annoying message that user should not see and the one with which you cannot do a lot.

As I said at the beginning there are three steps that have to be done in order to prevent these error messages. So, the first thing we need to do is to inform user when he or she makes a request for an invalid action. MVC provides a mechanism for this. Every controller has a virtual method called HandleUnknownAction. When you override this method you get the following signature:

protected override void HandleUnknownAction(string actionName)
{
    base.HandleUnknownAction(actionName);
}

HandleUnknownAction by default throws HttpException 404. This is not of much help to a user.

Instead of implementing this override in every controller, lets create a BaseController.cs that inherits from Controller class. It should look something like this:

public class BaseController : Controller
{
    protected override void HandleUnknownAction(string actionName)
    {
        Response.RedirectPermanent("~/Error");
    }
}

For this to work successfully we need to change inheritance for HomeController and ProductController. Instead of inheriting from Controller class make sure that they inherit from BaseController. In addition to this you will need to add ErrorController with the following contents:

public class ErrorController : BaseController
{
    //
    // GET: /Error/

    public ActionResult Index()
    {
        return View();
    }
}

Also add a view for the above Index action. I created a view that looks like this:

@{
    ViewBag.Title = "Index";
}
<h2>Error!</h2>
<div>You have requested an invalid resource!</div>

@Html.ActionLink("Return to home page", "Index", "Home")

Now, whenever a user makes a request for an action that does not exist, he or she will receive an error message from Error/Index. If you make a request for Product/New you will see this error in action.

What about if a user places a request to a controller and action that do not exist? What if a user requests /Customer or /Customer/List? Making a request to these resources will cause the Yellow Screen of Death. MVC does not have a facility similar to HandleUnknownAction. For this we need to do some extra work.

To understand what needs to be done we need to know how requests work in ASP.NET. Briefly it looks like this: When a user places a request for a resource, ASP.NET will first analyze this request for potential script exploits and other security related issues. During this step many features are prepared like HttpContext, Session, etc. After that HttpApplication is created and the lifecycle of an application begins through the set of events to which a user can subscribe. The routing system is initiated by the request. After the routing system completes the system invokes ControllerFactory. ControllerFactory will return a controller that will service the request. The controller on the other hand contains a mechanism called Action Invoker that will invoke the appropriate action. An action will process the request and return a result to a user.

As you may have guessed we need to work with ControllerFactory. In MVC there is a DefaultControllerFactory that is appropriate for most situations. However, in our situation this DefaultControllerFactory is not good enough. We need to create a custom controller factory that inherits from this DefaultControllerFactory.

public class CustomControllerFactory : DefaultControllerFactory
{
    public override IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controller = (IController) null;
        try
        {
            controller = base.CreateController(requestContext, controllerName);
        }
        catch (Exception)
        {
            requestContext.RouteData.Values["controller"] = "Error";
            requestContext.RouteData.Values["action"] = "Index";

            controller = base.CreateController(requestContext, "Error");

        }

        return controller;
    }
}

When MVC invokes CreateController method it will attempt to find a controller with the name specified in controllerName. If it find one it returns an instance of that controller. If it does not find a controller it throws an exception. Inside of this exception we get a chance to redirect a request to ErrorController.

Now all that is left is to register this new custom controller factory in global.asax and to add a catch-all route. In the Application_Start in your global.asax add the following:

protected void Application_Start()
{
    // You only need this line
    ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());

    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
}

We have now successfully replaced DefaultControllerFactory with our CustomControllerFactory. This will help us prevent errors when an invalid resource is requested. At the end there is one more thing we need to do. We have to register a catch-all route to prevent 404 error message when a user invokes a controller and action with a lot of parameters. If you try now to make a request that is something like /Customer/Edit/123/abc/John-Doe you will get a 404 error message. That is because you have sent too many parameters and there is no default route that can handle the request. For that reason you have to add the following route definition as the very last one (after Default route):

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );

        // Place this as the last route definition
        routes.MapRoute(
            name: "CatchAll",
            url: "{controller}/{action}/{id}/{*catchAll}",
            defaults: new {controller = "Error", action = "Index", id = UrlParameter.Optional}
        );
    }
}

Now when your user makes any kind of invalid request, he or she will receive an Error controller informing them that they requested invalid resource. Well, I lied a bit at the end. If you make the following request /Views/web.config you will receive an error message returned by the HttpNotFoundHandler. If you feel that you don’t like that error message you could implement your own handler that is based on HttpNotFoundHandler.

And that’s about it. I have partially tested this in my current project. In case I find a problem at some point I’ll promptly update the article with a possible solution.

Posted in ASP.NET MVC, Web Development | Tagged , , , , , , ,

Passing User Roles to HttpContext.User

Introduction

When logging users into a web application we usually have a tendency to use FormsAuthetnication.SetAuthCookie() method, passing it username and whether it is a persistent cookie or not. Later in your application when determining what a user can or cannot see you would use HttpContext.User.IsInRole() to determine whether a user belongs to a given role. Another method for querying user’s role(s) is by using Role provider’s IsUserInRole.

In the first case, the default behavior of HttpContext.User.IsInRole is to check an internal array of strings and to return true if user belongs to a group or false if array is null. If array is empty, the execution passes to ClaimsPrincipal.IsInRole which finally returns true or false. By default the IPrincipal implementation, in this case HttpContext.User, never receives a list of roles to which user belongs to.

With Roles provider the case is different. Instead of reading user’s roles from an underlying data source on each request, it caches the roles in a cookie. This is the default behavior and can be overridden either in web.config or programmatically.

There are times when you might not want to use Roles provider, but rather use your own security and data access logic. In that case you can apply the same logic as Roles provider and store roles in a cookie or you can store them in IPrincipal object. I am going to show you the latter approach by using FormsAuthenticationTicket and IHttpModule interface.

Implementation

Instead of using FormsAuthentication.SetAuthCookie to create the authentication ticket in the background, we can create FormsAuthenticationTicket manually and add it to the Cookies collection.

public void CreateAuthenticationCookie(HttpContextBase httpContext, UserDetails userDetails)
{
    var ticket = new FormsAuthenticationTicket(
        version: 1,
        name: userDetails.UserName,
        issueDate: DateTime.Now,
        expiration: DateTime.Now.AddSeconds(httpContext.Session.Timeout),
        isPersistent: false,
        userData: String.Join("|", userDetails.Roles));

    var encryptedTicket = FormsAuthentication.Encrypt(ticket);
    var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);

    httpContext.Response.Cookies.Add(cookie);
}

This version of FormsAuthenticationTicket class takes six parameter, but the most significant one here is userData. This parameter accepts a string value which we use to store user’s roles separated by a pipe. The ticket is then encrypted and added to the collection of cookies. That’s all there is to storing the roles in a cookie. The next step is to pass these roles to the GenericPrincipal. For that we can either register AuthenticateRequest in global.asax or implement IHttpModule. Instead of putting too much logic into global.asax and allowing future reuse, IHttpModule implementation is a much better choice. This implementation looks like the code below.

namespace Sample.Web.Infrastructure.Modules
{
    public class RolesProcessingModule : IHttpModule
    {
        /// <summary>
        /// Initializes a module and prepares it to handle requests.
        /// </summary>
        /// <param name="context">
        /// An <see cref="T:System.Web.HttpApplication"/> that provides access to the methods, 
        /// properties, and events common to all application objects within an ASP.NET application 
        /// </param>
        public void Init(HttpApplication context)
        {
            context.AuthenticateRequest += OnAuthenticateRequest;
        }

        private void OnAuthenticateRequest(object sender, EventArgs eventArgs)
        {
            try
            {
                var app = (HttpApplication) sender;
                var principal = app.Context.User;

                if (principal == null || !principal.Identity.IsAuthenticated) return;

                var cookie = app.Context.Request.Cookies[FormsAuthentication.FormsCookieName];

                if (cookie == null) return;

                var ticket = FormsAuthentication.Decrypt(cookie.Value);

                if (ticket == null) return;

                var roles = ticket.UserData.Split(new[] {"|"}, StringSplitOptions.RemoveEmptyEntries);
                var identity = principal.Identity;

                app.Context.User = new GenericPrincipal(identity, roles);
            }
            catch (Exception)
            {
                return;
            }
        }

        /// <summary>
        /// Disposes of the resources (other than memory) used by the module that 
        /// implements <see cref="T:System.Web.IHttpModule"/>.
        /// </summary>
        public void Dispose()
        { }
    }
}

We are subscribing for AuthenticateRequest event. This event is also used by FormsAuthentication when it processes user’s, therefore this is a good place to use it.

It is necessary first to check whether user is authenticated or not. If user is authenticated a cookie is retrieved and its value is decrypted in order to form a FormsAuthenticationTicket. Once this is successfully done, the roles are split by using pipe (which we set earlier) as a separator. The array is used to create an object of type GenericalPrincipal which is then assigned to HttpContext.User.

All that is left now is to register our module in web.config. This is done in system.webServer.modules (I am using IIS 7.5 and Integrated mode, but if you are using IIS 6 you should use system.web.modules section):

  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
      <add type="Sample.Web.Infrastructure.Modules.RolesProcessingModule, Sample.Web" name="RolesProcessingModule"/>
    </modules>
  </system.webServer>

And that’s all there is to it. Now whenever a user makes a request the roles to which he or she belongs to will be added to HttpContext.User and you can use HttpContext.User.IsInRole method to query roles permissions.

I shall upload this code to CodePlex so that you can examine it.

Posted in Development, Web Development | Tagged , , , , , ,

Implementing Custom Route for Legacy URLs in MVC

There are times when default routing mechanism is simply not sufficient for certain scenarios. For example, you migrated from a Web Forms application to MVC, but the problem is that many users have kept the legacy URLs to help files or some other content. In cases like this where you still have old HTML and you still intend to use them, you have two possibilities. One is to avoid the routing system and serve disk files directly or you can implement your own route that handles old legacy files.

The problem with the first option is that you are letting users know what the folder structure is. Second problem is that you cannot keep track about the usage of legacy links or access to those files in general. This means that action filters do not play any role in the system. It would be very nice if you could employ a controller and an action to load and display file contents to the users. For that purpose you need to implement your own route by inheriting from RouteBase abstract class.

RouteBase class

RouteBase is an abstract base class that requires you to implement two methods: GetRouteData and GetVirtualPath.

GetRouteData is used for processing incoming URLs. It takes HttpContextBase as an argument and returns RouteData object that is used by the routing system to determine whether a route can handle the request or not. If you return null, the route is ignored and ASP.NET routing system asks another route to handle the case.

GetVirtualPath is used for generating outgoing links. These are the ones that you generate by calling Html.ActionLink helper method or methods of UrlHelper class.

HelpRoute implementation

To be clear up front, this is not a fully featured implementation. It is not very versatile because the aim was to show how to implement your own route. There are certain expectations that have to be satisfied. For example .html files can be in one directory and not in additional subdirectories. The segments expected by HelpRoute need to be called controller, action and topic.

This implementation of RouteBase abstract class, named HelpRoute is used to present legacy .html files to users via controller and action. The constructor signature looks like this:

public HelpRoute(string url, string legacyUrl, IRouteHandler routeHandler)
public HelpRoute(string url, string legacyUrl)

The url parameter expects an MVC-style route. The same thing like the default route. It is a mandatory field and you need to pass it a string with a value like Help/Show/{topic}. Because there is no defaults, controller and action need to be specified. The topic segment is determined internally based on the legacy URL’s file name.

The legacyUrl parameter is the location of the .html files. You can specify it either as ~/LegacyContent/{filename}.html or ~/LegacyContent/SampleHelpFile.html. In the first case the route will pick every file that ends with .html extension while in the latter case only one specific case will be handled by the route. Note that leading ~ is needed for legacy URLs.

The third parameter allows you to specify route handler implementation. If you don’t specify this value, the default route handler, MvcRouteHandler is used.

The implementation of GetRouteData looks like this:

public override RouteData GetRouteData(HttpContextBase httpContext)
{
    RouteData routeData;
    var requestedUrl = httpContext.Request.AppRelativeCurrentExecutionFilePath;

    if (IsLegacyUrl(requestedUrl))
    {
        routeData = BuildFromLegacyUrl(requestedUrl);
    }
    else
    {
        routeData = BuildFromMvcUrl(requestedUrl);
    }

    return routeData;
}

From the above implementation you can see that GetRouteData can handle requests that come as both legacy URL or MVC-style. We take the request and check whether it is a legacy URL in which case we call BuildFromLegacyUrl() method, or we call BuildFromMvcUrl(). Both methods must return RouteData object if the request can be handled, otherwise null is returned back to the routing system.

Inside of BuildFromLegacyUrl method we need to check whether the request matches the supplied legacyUrl template that was supplied to the constructor. If the request is a match the RouteData object is created.

The check for the template is performed by a method called IsLegacyTemplateMatch. This method performs three checks before deciding on true/false return value. The first check is the folder comparison between template specified folder and requested URL one. If this is a match we performs second check. This determines whether {filename} placeholder was used. If it is used true is returned back to the calling method, otherwise we check whether the page requested matches the page specified in legacyUrl.

The third check is executed if the first check returns false. The reason for this is that a user can click on a link inside of an HTML page that leads to another page. Because the previous request would not be ~/LegacyContent/Afile.html, but rather /Help/Show/Afile, we need to allow users to continue browsing the help file. For that reason we make sure that requested URL matches url specified as the parameter to the constructor.

From this point on a BuildRouteData method is called and the RouteData is built and returned. And with that the request is completed and the routing system can use HelpRoute to serve the request.

Another required method for implementation is GetVirtualPath. Now, this method is used when generating outgoing URLs and for our scenario it is not necessary. However, I have implemented it with a sample of what might it would look like if we called Html.ActionLink or Content method of a UrlHelper object.

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
    var virtualPathData = (VirtualPathData) null;

    if (values.ContainsKey("topic"))
    {
        var urlHelper = new UrlHelper(requestContext);
        var content = urlHelper.Content(values.GenerateMvcUrl("topic"));
        virtualPathData = new VirtualPathData(this, content);
    }

    return virtualPathData;
}

The controller and action that handle the request look like this:

public class HelpController : Controller
{
    //
    // GET: /Help/Show/{topic}
    public ActionResult Show(string topic)
    {
        var legacyUrl = String.Format("~/LegacyContent/{0}.html", topic);
        var physicalFileLocation = Server.MapPath(legacyUrl);
        var contents = System.IO.File.ReadAllText(physicalFileLocation);

        return Content(contents);
    }
}

Notes at the end

When you initially make a request such as /Help/Show/RouteBase and then click on GetRouteData, the URL will be /Help/Show/GetRouteData.html. Despite this, when HelpRoute processes the request it will pass GetRouteData as a topic parameter’s value. However, every single request is served via Help/Show even if you make a direct request via /LegacyContent/RouteBase.html for example.

You can download the source code for HelpRoute from CodePlex. In this project you will find in the Infrastructure folder a HelpRoute.cs. In addition to this, there is a unit test project that tests HelpRoute.

Posted in ASP.NET MVC, Development, Web Development | Tagged , , , ,

Routing in MVC – Part 4

Generating Outgoing URLs

Processing incoming URLs is just a half of the story. The other half is about generating outgoing links that your users can use to click on the links or to submit the forms. The routing system still depends on the route definitions when building the links.

In order for a link to generate successfully, three conditions must be satisfied:

  1. You must supply a value for every segment in a URL pattern. The routing system will first take into account the values that we provide, next it will use the values that it already has as part of the current request, and at the end it will rely upon default values.
  2. If you declare a default value which does not appear in a URL pattern then you either do not use it in your outgoing URL generation or you use with the same value that you used to declare the default value.
  3. The constraints defined on a route must be respected.

There are two things that need to be clarified here. The first one is from point 1 which says “… next it will use the values that is already has as part of the current request…”. The point here is that the routing system will reuse those segments that it already has only if they appear before the segment being changed. For example, lets say we have a following URL /Products/List/Jackets/Red/2. Now, Products is our controller and List is the action that we use to display a grid with product called Jackets. User chose to see Red jackets and is on page 2 of our web grid. To move to the third page we can use Html.ActionLink to generate a link like

Html.ActionLink("3", "List", "Jackets", new { page = 3 })

When user clicks on this link he/she will get the link /Products/List/Jackets/Red/3. We did not specify category or color, but it is still present. The reason for this is that the routing system was smart enough to pick these up. However, if a user changes a category from Jackets to Shoes, then everything after the category will be “reset” and you need to provide the values.

Relying on the routing system is nice, but it can cause problems for the next person who goes through your code. He/she will usually expect a controller Products to have action List which takes one parameter named page. For this reason it is best to supply all the values to remove any source of ambiguity.

The next point to discuss is point 2. If you decide to declare a route with a default value which does not appear in a URL pattern, then you either do not use it or if you do, you must specify exactly the same value for it. For example,

route.MapRoute(null, "{controller}/{action}/{page}", new { someDefaultValue = 123 });

You cannot call someDefaultValue with any other value. It has to be 123 or you don’t use this parameter at all.

Next time I am going to write about extending routing system with custom RouteBase implementation.


References

Pro ASP.NET MVC 4

Posted in ASP.NET MVC, Development | Tagged , , ,

Routing in MVC–Part 3

Namespaces

When using MapRoute method you can also specify two more parameters: constraints and namespaces.

In large MVC projects you might have one or more areas defined that all have HomeController. Imagine that you have the following case:

http://www.mydomain.com/Home/Index
http://www.mydomain.com/Admin/Home/Index
http://www.mydomain.com/Members/Home/Index

We have two areas and one standard location. All three of them have HomeController class with Index action. When a user attempts to access http://www.mydomain.com/Admin/Home/Index an exception is thrown.

NamespaceCollision

(The above error message is different to the URLs I listed. The idea is the same, so please ignore the differences)

In the above case we have three HomeController classes in a project. The error message is pretty detailed and it points you in the right direction. In order to fix this type of an error we need to tell MVC which HomeController to load first. For this we need to specify namespaces parameter. This parameter is an array of strings that specify where to search. It’s important to remember that the search is not like in the case of routes where first route found is served. Rather in the case of namespaces every single one is taken into consideration.

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    namespaces: new[] { "Appi.Web.Controllers" }
);

When a user makes a request for http://www.mydomain.com/Home/Index the HomeController that resides in Appi.Web.Controllers will be invoked and served to a user. What about the other HomeControllers? What if a user wants to access HomeController in AdditionalControllers namespace? Adding this namespace to a list of namespaces will throw exactly the same exception. In this case we need to specify a route with an explicit Home controller.

routes.MapRoute(
    name: "ServiceController",
    url: "Home/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    namespaces: new[] { "Appi.Web.ServiceControllers" }
);

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    namespaces: new[] { "Appi.Web.Controllers" }
);

With this definition a user will be served by HomeController in ServiceControllers only if he or she makes an explicit request for Home, like http://www.mydomain.com/Home. If Home is not specified then HomeController from Controllers namespace is used.

Constraints

Constraints allow you to apply certain rules to your route registration. The rules can be applied either using regular expression or adding an object which implements IRouteConstraint interface. For example,

routes.MapRoute(
    name: null,
    url: “{controller}/{id}/{action}”,
    defaults: new { controller = “Customer”, action = “Details” },
    constraints: new { controller = “^Customer$”, action = “^Details$|^Info$” }
);

This route can only be invoked if a user places a request to Customer controller and either Details or Info action is invoked, eg http://mydomain.com/Customer/17/Info. This is an important information about constraints. Every condition is evaluated and every condition must be true.

Apart from regular expression constraints, there is HttpMethodConstraint class that comes with MVC. This class implements an interface IRouteConstraint and it allows you to specify which HTTP methods are allowed to be invoked on a route. For example,

routes.MapRoute(
    name: null,
    url: “{controller}/{id}/{action}”,
    defaults: new { controller = “Customer”, action = “Details” },
    constraints: new { controller = “^Customer$”, action = “^Details$|^Info$”, httpMethod = new HttpMethodConstraint(“GET”) }
);

Now our root also adds a constraint that requires the HTTP method be GET. Other methods like POST, DELETE, PUT, HEAD, PATCH, etc. are not allowed. The parameters httpMethod is just there to help us distinguish this constraint from other value-based constraints.

Important thing to remember regarding this constraint is that, like any other constraint it is executed much earlier during the pipeline execution cycle. If you have actions in your controller decorated with HttpPostAttribute they will never be reached because actions execute much later. If you attempt to execute a POST method when there is a constraint that allows only GET method, you will get an error from IIS server, specifically it is HTTP Error 405.0 – Method Not Allowed.

In order to allow both GET and POST you can pass a list of string value parameters to the constructor like new HttpMethodConstraint(“GET”, “POST”).

You can also create your own custom route constraints by implementing IRouteConstraint interface which contains only one method, Match, and which returns Boolean result. If Match was a success you would return true, otherwise it is false. The following sample demonstrates how to implement route constraint that checks whether the request came from the local computer.

public class LocalModeOnlyRouteConstraint : IRouteConstraint
{
    protected virtual bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        return httpContext.Request.IsLocal;
    }

    bool IRouteConstraint.Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        return Match(httpContext, route, parameterName, values, routeDirection);
    }
}

This simple route constraint uses Request.IsLocal to check whether the request is coming from the local computer or not. This sort of constraint could be applied on specific administrative routes that would allow the execution to take place only on the server directly. Other constraints, for example could be based on a REST service by http://freegeoip.net that allows you to check user’s country and prevent access to a route if a country is on your blacklist.

For the end here is a simple HTML helper that I wrote to help me display everything about a route. The working might be similar to Phil Haacked’s RouteDebugger 2.0. The reason I wrote is to have a quick-and-dirty way of displaying the route matches. It will display a table with every route that it finds and it highlights the last cell in red or green depending on which route(s) could server the request. You can download this project on CodePlex (MVC Developer on CodePlex)

Next time I’m going to write about outgoing URLs and how they get generated.


References

Pro ASP.NET MVC 4

Posted in ASP.NET MVC, Development | Tagged , , ,