Alexander Manekovskiy

Automating Automation

Configuring Web Forms Routing With Custom Attributes

1/13/2013 Update: Now PhysicalFile property is filled and updated automatically, using T4 template. Say good-bye to issues caused by typos and copy-pasting.

Recently I had to add routing to existent ASP.NET Web Forms application. I was (and I suppose I’m still) new to this thing so I started from Walkthrough: Using ASP.NET Routing in a Web Forms Application and it seemed fine until I started coding.

The site was nothing special but approximately 50 pages. And when I started configuring all these pages it felt wrong - I was lost in all these route names, defaults and constraints. If it felt wrong, I thought, why not to try something else. I googled around and found a pretty good thing - ASP.NET FriendlyUrls. Scott Hanselman wrote about this in his Introducing ASP.NET FriendlyUrls - cleaner URLs, easier Routing, and Mobile Views for ASP.NET Web Forms post. At first glance it looked far easier and better, but I wanted to use RouteParameters for my datasource controls on pages. ASP.NET FriendlyUrls are providing only “URL segment” concept - string that could be extracted from URL (string between ‘/’ characters in URL). URL Segments could not be constrained and thus automatically validated. Also, segments could not have names, so my idea to use RouteParameter would be killed if I’d go with ASP.NET FriendlyUrls.

At the end of this little investigation I thought that it would be easier to tie together route configuration with page class via custom attribute and conventionally named properties for defaults and constraints. So every page class gets its routing configuration as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace RoutingWithAttributes.Foo
{
 [MapToRoute(RouteUrl = "Foo/Edit/{id}")]
  public partial class Edit : Page
  {
      public static RouteValueDictionary Defaults
      {
          get
          {
              return new RouteValueDictionary { { "id", "" } };
          }
      }

      public static RouteValueDictionary Constraints
      {
          get
          {
              return new RouteValueDictionary { { "id", "^[0-9]*$" } };
          }
      }
  }
}

The code above states that Edit page in folder Foo of my RoutingWithAttributes web application will be accessible through http://<application-url>/Foo/Edit hyperlink with optional id parameter. Default value for id parameter is empty string but it should be integer number if provided.

For me this works better, it is self describing and I’m not forced to go to some App_Start\RoutingConfig.cs file and search for it. Now how it is working under the hood? Nothing new or special - just a bit of reflection on Application_Start event. And routes are still registered with RouteCollection.MapPageRoute method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected void Application_Start(object sender, EventArgs e)
{
  RouteConfig.RegisterRoutes(RouteTable.Routes);
}

public class RouteConfig
{
  public static void RegisterRoutes(RouteCollection routes)
  {
      var mappedPages = Assembly.GetAssembly(typeof (RouteConfig))
              .GetTypes()
              .AsEnumerable()
              .Where(type => type.GetCustomAttributes(typeof (MapToRouteAttribute), false).Length == 1);

      foreach (var pageType in mappedPages)
      {
          var defaultsProperty = pageType.GetProperty("Defaults");
          var defaults = defaultsProperty != null ? (RouteValueDictionary)defaultsProperty.GetValue(null, null) : null;

          var constraintsProperty = pageType.GetProperty("Constraints");
          var constraints = constraintsProperty != null ? (RouteValueDictionary)constraintsProperty.GetValue(null, null) : null;

          var dataTokensProperty = pageType.GetProperty("DataTokens");
          var dataTokens = dataTokensProperty != null ? (RouteValueDictionary)dataTokensProperty.GetValue(null, null) : null;

          var routeAttribute = (MapToRouteAttribute)pageType.GetCustomAttributes(typeof(MapToRouteAttribute), false)[0];

          if(string.IsNullOrEmpty(routeAttribute.RouteUrl))
              throw new NullReferenceException("RouteUrl property cannot be null");

          if (string.IsNullOrEmpty(routeAttribute.PhysicalFile))
              throw new NullReferenceException("PhysicalFile property cannot be null");

          if(!VirtualPathUtility.IsAppRelative(routeAttribute.PhysicalFile))
              throw new ArgumentException("Property should be application relative URL", "PhysicalFile");

          routes.MapPageRoute(pageType.FullName, routeAttribute.RouteUrl, routeAttribute.PhysicalFile, true, defaults, constraints, dataTokens);
      }
  }
}

Route name is equal to the FullName property of page type. Since Type.FullName includes both namespace and class name it guarantees route name uniqueness across the application.

To utilize route links generation I had to create two extension methods for Page class. These methods are just wrappers for Page.GetRouteUrl method.

1
2
3
4
5
6
7
8
9
10
11
12
public static class PageExtensions
{
  public static string GetMappedRouteUrl(this Page thisPage, Type targetPageType, object routeParameters)
  {
      return thisPage.GetRouteUrl(targetPageType.FullName, routeParameters);
  }

  public static string GetMappedRouteUrl(this Page thisPage, Type targetPageType, RouteValueDictionary routeParameters)
  {
      return thisPage.GetRouteUrl(targetPageType.FullName, routeParameters);
  }
}

So now I can generate link to Foo.Edit page as follows:

1
    <a href='<%= Page.GetMappedRouteUrl(typeof(RoutingWithAttributes.Foo.Edit), new { id = 1 }) %>'>Foo.Edit</a>

And it will produce http://<application-url>/Foo/Edit/1 link.

Described approach helped me to accomplish task without frustration and I’m satisfied with the results.

Code for this article is hosted on GitHub feel free to use it if you liked the idea.

Comments