MVC Routes and Magic Strings == “No Good”

I live and breathe with ASP.NET MVC, but from the very beginning I’ve been very uncomfortable with the magic string nature of routes, controllers, and actions. I long ago stopped using the various “action” extensions because they are way too prone to fat finger screw ups and moved nearly 100% to using route extensions and putting my routes names into constants so as not to have any chance of fat finger issues, but recently, after reviewing the source code for MVC2, specifically the “LabelFor” extension, I realized there was even another way to limit fat finger screw ups via routing extensions and lambdas.

Note: After some remarks from readers, I have modified the original post

a. Add extensions for enabling the new MapRoute extension

public static class RouteCollectionExtensions
{
  public static System.Web.Routing.Route MapRoute<TController>(this System.Web.Routing.RouteCollection routes, string name, string url, Expression<Func<TController, System.Web.Mvc.ActionResult>> action) where TController : System.Web.Mvc.Controller
  {
    return routes.MapRoute<TController>(name, url, action, (object)null /* defaults */);
  }

  public static System.Web.Routing.Route MapRoute<TController>(this System.Web.Routing.RouteCollection routes, string name, string url, Expression<Func<TController, System.Web.Mvc.ActionResult>> action, object defaults) where TController : System.Web.Mvc.Controller
  {
    return routes.MapRoute<TController>(name, url, action, defaults, (object)null /* constraints */);
  }

  public static System.Web.Routing.Route MapRoute<TController>(this System.Web.Routing.RouteCollection routes, string name, string url, Expression<Func<TController, System.Web.Mvc.ActionResult>> action, object defaults, object constraints) where TController : System.Web.Mvc.Controller
  {
    if (routes == null) throw new ArgumentNullException("routes");
    if (url == null) throw new ArgumentNullException("url");

    System.Web.Routing.RouteValueDictionary defaultValues = new System.Web.Routing.RouteValueDictionary(defaults);

    Type type = typeof(TController);

    #region controllerName
    string controllerName = type.Name;

    if (controllerName.EndsWith("Controller", StringComparison.InvariantCultureIgnoreCase)) controllerName = controllerName.Substring(0, controllerName.Length - "Controller".Length);

    defaultValues["controller"] = controllerName;
    #endregion

    #region actionName
    System.Reflection.MethodInfo methodInfo = ((MethodCallExpression)action.Body).Method;

    string actionName = methodInfo.Name;

    ActionNameAttribute[] actionNameAttributes = (ActionNameAttribute[])methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), false);

    if ((actionNameAttributes != null) && (actionNameAttributes.Length > 0)) actionName = actionNameAttributes[0].Name;

    defaultValues["action"] = actionName;
    #endregion

    System.Web.Routing.Route route = new System.Web.Routing.Route(url, new MvcRouteHandler())
    {
      Defaults = defaultValues,
      Constraints = new System.Web.Routing.RouteValueDictionary(constraints),
      DataTokens = new System.Web.Routing.RouteValueDictionary()
    };

    #region controllerNamespace
    string controllerNamespace = type.FullName;

    controllerNamespace = controllerNamespace.Substring(0, controllerNamespace.Length - (type.Name.Length + 1));

    route.DataTokens["Namespaces"] = new string[] { controllerNamespace };
    #endregion

    routes.Add(name, route);

    return route;
  }
}

b. Create a routes class:

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

    Account.RegisterRoutes(routes);
    Home.RegisterRoutes(routes);
  }
}

c. Create a routes class for the HomeController

public partial class Routes
{
  public partial class Home
  {
    public class RouteNames
    {
      public static readonly string Index = Guid.NewGuid().ToString("D");
      public static readonly string About = Guid.NewGuid().ToString("D");
    }

    protected static readonly string URLRoot = "";

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.MapRoute<Controllers.HomeController>(RouteNames.About, URLRoot + "About", controller => controller.About());
      routes.MapRoute<Controllers.HomeController>(RouteNames.Index, URLRoot, controller => controller.Index());
    }
  }
}

Note: The route names are totally random because they are just meant to be unique keys pointing to specific routes

d. Create a routes class for the AccountController

public partial class Routes
{
  public partial class Account
  {
    public class RouteNames
    {
      public static readonly string LogOn = Guid.NewGuid().ToString("D");
      public static readonly string LogOff = Guid.NewGuid().ToString("D");
      public static readonly string Register = Guid.NewGuid().ToString("D");
      public static readonly string ChangePassword = Guid.NewGuid().ToString("D");
      public static readonly string ChangePasswordSuccess = Guid.NewGuid().ToString("D");
    }

    protected static readonly string URLRoot = "Account";

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.MapRoute<Controllers.AccountController>(RouteNames.LogOn, URLRoot + "LogOn", c => c.LogOn());
      routes.MapRoute<Controllers.AccountController>(RouteNames.LogOff, URLRoot + "LogOff", c => c.LogOff());
      routes.MapRoute<Controllers.AccountController>(RouteNames.Register, URLRoot + "Register", c => c.Register());
      routes.MapRoute<Controllers.AccountController>(RouteNames.ChangePassword, URLRoot + "ChangePassword", c => c.ChangePassword());
      routes.MapRoute<Controllers.AccountController>(RouteNames.ChangePasswordSuccess, URLRoot + "ChangePasswordSuccess", c => c.ChangePasswordSuccess());
    }
  }
}

e. Replace the register routes in Global.asax.cs:

public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    AreaRegistration.RegisterAllAreas();

    Routes.RegisterRoutes(RouteTable.Routes);
  }
}

f. Change all usages of “Action” extensions to “Route” extensions
    In LogOnUserControl.ascx change:
       Html.ActionLink("Log Off", "LogOff", "Account")
    To
      Html.RouteLink("Log Off", Routes.Account.RouteNames.LogOff)

    In AccountController.cs change:
       return RedirectToAction("Index", "Home");
    To
       return RedirectToRoute(Routes.Home.RouteNames.Index);

Published 03/01/2010 16:35:09 (UTC) by Ron Muth

kick it on DotNetKicks.com  Shout it  vote it on WebDevVote.com


Serialization In ASP.NET MVC

I’ve been doing a lot more with jQuery in ASP.NET MVC than I ever have before. One of the biggest problems I’ve had is in the sending back of JSON to jQuery ajax calls, it seems that the built in MVC action result is not to friendly with dates and/or forms submitted with uploaded files, so with some help of an article @ http://msmvps.com/blogs/omar/archive/2008/10/03/create-rest-api-using-asp-net-mvc-that-speaks-both-json-and-plain-xml.aspx I’ve come up with my own SerializeResult<T>

 

Usage:

To send back JSON for a ajax submit without a file upload:

result = new SerializeResult<Models.CheckPassword.Response>(model.Result, enSerializeResultFormat.Json);
 

To Send back JSON for an ajax submit with a file upload:

result = new SerializeResult<Models.UploadFile.ReceiveFile.Response>(model.Result, enSerializeResultFormat.JsonTextArea);

 

All you have to do is decorate your classes with “[DataContract]” and “[DataMember]” attributes

 

ex:

    [DataContract]
    public class Client
    {
      [DataMember] public int? ClientID;
      [DataMember] public string CompanyName;

      [DataMember] public decimal PriorPriorYearSalesTotal;
      [DataMember] public decimal PriorYearSalesTotal;
      [DataMember] public decimal CurrentYearSalesTotal;
      [DataMember] public decimal SalesTotal;

      public Client()
      {
        ClientID = null;
        CompanyName = string.Empty;

        PriorPriorYearSalesTotal = 0;
        PriorYearSalesTotal = 0;
        CurrentYearSalesTotal = 0;
        SalesTotal = 0;
      }
    }

Here is the

Code:

 

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Security.Principal;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Runtime.Serialization.Json;

//http://msmvps.com/blogs/omar/archive/2008/10/03/create-rest-api-using-asp-net-mvc-that-speaks-both-json-and-plain-xml.aspx

namespace ISI.Libraries.MVC
{
  public enum enSerializeResultFormat
  {
    Auto,
    Xml,
    JavaScript,
    Json,
    JsonTextArea
  }

  [AspNetHostingPermission(System.Security.Permissions.SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
  [AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
  public class SerializeResult<T> : ActionResult
  {
    private static UTF8Encoding UTF8 = new UTF8Encoding(false);

    public T Data { get; set; }
    public Type[] IncludedTypes = new Type[] { typeof(object) };
    public enSerializeResultFormat SerializeFormat = enSerializeResultFormat.Auto;


    public SerializeResult(T data, Type[] extraTypes, enSerializeResultFormat serializeFormat)
    {
      this.Data = data;
      this.IncludedTypes = extraTypes;
      this.SerializeFormat = serializeFormat;
    }
    public SerializeResult(T data, enSerializeResultFormat serializeFormat) : this(data, new Type[] { typeof(object) }, serializeFormat) { }
    public SerializeResult(T data, Type[] extraTypes) : this(data, extraTypes, enSerializeResultFormat.Auto) { }
    public SerializeResult(T data) : this(data, new Type[] { typeof(object) }) { }


    public override void ExecuteResult(ControllerContext filterContext)
    {
      if(SerializeFormat == enSerializeResultFormat.Auto)
      {
        SerializeFormat = ((filterContext.HttpContext.Request.Headers["Content-Type"] ?? string.Empty).ToLower().Contains("application/json") ? enSerializeResultFormat.Json : enSerializeResultFormat.Xml);
      }

      switch (SerializeFormat)
      {
        case enSerializeResultFormat.Xml:
          using (System.IO.MemoryStream oStream = new System.IO.MemoryStream())
          {
            ISI.Libraries.XML.Serialize<T>(this.Data, oStream);

            new ContentResult { ContentType = "text/xml", Content = UTF8.GetString(oStream.ToArray()), ContentEncoding = UTF8 }.ExecuteResult(filterContext);
          }
          break;

        case enSerializeResultFormat.Json:
          using (System.IO.MemoryStream oStream = new System.IO.MemoryStream())
          {
            new DataContractJsonSerializer(typeof(T)).WriteObject(oStream, this.Data);

            new ContentResult { ContentType = "application/json", Content = UTF8.GetString(oStream.ToArray()), ContentEncoding = UTF8 }.ExecuteResult(filterContext);
          }
          break;

        case enSerializeResultFormat.JsonTextArea:
          using (System.IO.MemoryStream oStream = new System.IO.MemoryStream())
          {
            new DataContractJsonSerializer(typeof(T)).WriteObject(oStream, this.Data);

            new ContentResult { ContentType = "text/html", Content = string.Format("<textarea>{0}</textarea>", UTF8.GetString(oStream.ToArray())), ContentEncoding = UTF8 }.ExecuteResult(filterContext);
          }
          break;

        case enSerializeResultFormat.JavaScript:
          new JsonResult { Data = this.Data }.ExecuteResult(filterContext);
          break;
      }
    }
   
  }
}
Published 06/01/2009 15:12:09 (UTC) by Ron Muth

kick it on DotNetKicks.com  Shout it  vote it on WebDevVote.com