Integrated Solutions, Inc.

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 by Ron Muth


Extending Enumerations (fun with Enum<enum> and Attributes)

Enums are just great, but not very user friendly once you get past simple one word names. In this article I will show you how to make them far more usable via Attributes. We will be able to give them a user friendly name, parse strings to enum based on the enum’s value, member description, and user friendly description, and created orders lists and dictionaries.

public enum enRates
{
  Main = 0, //Hourly
  OffSiteFullDay = 22, //Off Site Full Day
  OnSiteFullDay = 21, //On Site Full Day
  OffSite = 202, //Off Site Hourly
  OnSite = 201, //On Site Hourly
}

Notice how the values are not orders the way I want them and the names don’t match the user descriptions.

Now lets see what we can do by extending them through attributes:

public enum enRates
{
  [EnumInformation("Hourly", true, 1)] Main = 0,
  [EnumInformation("Off Site Full Day", true, 4)] OffSiteFullDay = 22,
  [EnumInformation("On Site Full Day", true, 5)] OnSiteFullDay = 21,
  [EnumInformation("Off Site Hourly", true, 2)] OffSite = 202,
  [EnumInformation("On Site Hourly", true, 3)] OnSite = 201,
}

Now lets see what we can do with these attributes

a. Get a user friendly description for an enum

Console.WriteLine("Enum<enRates>.Description(enRates.Main) = {0}", Enum<enRates>.Description(enRates.Main));

gets:

Enum<enRates>.Description(enRates.Main) = Hourly

b. Parse values to get enum

Console.WriteLine("Enum<enRates>.Parse(\"Main\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("Main", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"Hourly\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("Hourly", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"0\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("0", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"On Site Full Day\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("On Site Full Day", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"OnSiteFullDay\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("OnSiteFullDay", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"UnKnown\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("UnKnown", enRates.OnSiteFullDay));

gets:

Enum.Parse("Main", enRates.OnSiteFullDay) = Main
Enum.Parse("Hourly", enRates.OnSiteFullDay) = Main
Enum.Parse("0", enRates.OnSiteFullDay) = Main
Enum.Parse("On Site Full Day", enRates.OnSiteFullDay) = OnSiteFullDay
Enum.Parse("OnSiteFullDay", enRates.OnSiteFullDay) = OnSiteFullDay
Enum.Parse("UnKnown", enRates.OnSiteFullDay) = OnSiteFullDay

c. Get an ordered list for an enum (notice the order its ordered by the third value in the attribute, not the value of the member)

Console.WriteLine("ToList:");
List<enRates> list = Enum<enRates>.ToList();
foreach (enRates rate in list)
{
  Console.WriteLine("  {0}", rate);
}

gets:

ToList:
  Main
  OffSite
  OnSite
  OffSiteFullDay
  OnSiteFullDay

d. Get an ordered user friendly list of descriptions for an enum

Console.WriteLine("ToStringList:");
List<string> stringList = Enum<enRates>.ToStringList();
foreach (string rate in stringList)
{
  Console.WriteLine("  {0}", rate);
}

gets:

ToStringList:
  Hourly
  Off Site Hourly
  On Site Hourly
  Off Site Full Day
  On Site Full Day

e. Get an ordered user friendly dictionary with the member string value and user friendly value

Dictionary<string, string> stringDictionary = Enum<enRates>.ToStringDictionary();
foreach (KeyValuePair<string, string> rate in stringDictionary)
{
  Console.WriteLine("  {0} = {1}", rate.Key, rate.Value);
}

gets:

ToStringDictionary:
  Main = Hourly
  OffSite = Off Site Hourly
  OnSite = On Site Hourly
  OffSiteFullDay = Off Site Full Day
  OnSiteFullDay = On Site Full Day

f. Get an ordered user friendly dictionary with the int value of the member and user friendly value

Dictionary<int, string> dictionary = Enum<enRates>.ToDictionary();
foreach (KeyValuePair<int, string> rate in dictionary)
{
  Console.WriteLine("  {0} = {1}", rate.Key, rate.Value);
}

gets:

ToDictionary:
  0 = Hourly
  202 = Off Site Hourly
  201 = On Site Hourly
  22 = Off Site Full Day
  21 = On Site Full Day

g. Get an ordered user friendly dictionary with the member and user friendly value

Dictionary<enRates, string> valueDictionary = Enum<enRates>.ToValueDictionary();
foreach (KeyValuePair<enRates, string> rate in valueDictionary)
{
  Console.WriteLine("  {0} = {1}", rate.Key, rate.Value);
}

gets:

ToValueDictionary:
  Main = Hourly
  OffSite = Off Site Hourly
  OnSite = On Site Hourly
  OffSiteFullDay = Off Site Full Day
  OnSiteFullDay = On Site Full Day

Here’s the source code:

[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public class EnumInformationAttribute : Attribute
{
  public string Description { get; internal set; }
  public bool Active { get; internal set; }
  public int? Order { get; internal set; }

  public EnumInformationAttribute(string description, bool active, int? order)
  {
    Description = description;
    Active = active;
    Order = order;
  }

  public EnumInformationAttribute(string description, bool active, int order) : this(description, active, (int?)order) { }

  public EnumInformationAttribute(string description, bool active) : this(description, active, null) { }

  public EnumInformationAttribute(string description) : this(description, true, null) { }
}

public class EnumParseException : Exception
{
  public EnumParseException() : base() { }
  public EnumParseException(string message) : base(message) { }
  public EnumParseException(string message, Exception innerException) : base(message, innerException) { }
}

public static class Enum<T>
{
  #region EnumInformation
  public class EnumInformation
  {
    public T Value { get; internal set; }
    public string DefaultDescription { get; internal set; }
    public string WithSpaces { get; internal set; }
    public bool Active { get; internal set; }
    public int Order { get; internal set; }

    internal EnumInformation(T value, string defaultDescription, string withSpaces, bool active, int order)
    {
      Value = value;
      DefaultDescription = defaultDescription;
      WithSpaces = withSpaces;
      Active = active;
      Order = order;
    }

    internal EnumInformation(T value) : this(value, value.ToString(), RemoveTitleCase(value.ToString()), true, Convert.ToInt32(value)) { }
  }
  #endregion

  #region Static Constructor
  internal static List<EnumInformation> _List = null;
  internal static Dictionary<T, EnumInformation> _Descriptions = null;
  internal static Dictionary<string, T> _LookupValues = null;

  static Enum()
  {
    string key;
    Type type = typeof(T);
    EnumInformationAttribute[] enumInformationAttributes = null;

    EnumInformation enumInformation = null;

    _List = new List<EnumInformation>();
    _Descriptions = new Dictionary<T, EnumInformation>();
    _LookupValues = new Dictionary<string, T>();

    if (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) type = (new System.ComponentModel.NullableConverter(type)).UnderlyingType;

    if (!(type.IsEnum)) throw new Exception("Cannot create Enum<T> when T is not a Enum");

    foreach (T value in Enum.GetValues(type))
    {
      enumInformation = new EnumInformation(value);
      enumInformationAttributes = (EnumInformationAttribute[])(type.GetField(value.ToString()).GetCustomAttributes(typeof(EnumInformationAttribute), false));

      if (enumInformationAttributes.Length > 0)
      {
        enumInformation.DefaultDescription = enumInformationAttributes[0].Description;
        enumInformation.WithSpaces = enumInformationAttributes[0].Description;
        enumInformation.Active = enumInformationAttributes[0].Active;
        enumInformation.Order = enumInformationAttributes[0].Order ?? enumInformation.Order;
      }

      _List.Add(enumInformation);
      _Descriptions.Add(value, enumInformation);

      key = Convert.ToInt32(value).ToString().ToLower(); if (!_LookupValues.ContainsKey(key)) _LookupValues.Add(key, value);
      key = enumInformation.Value.ToString().ToLower(); if (!_LookupValues.ContainsKey(key)) _LookupValues.Add(key, value);
      key = enumInformation.DefaultDescription.ToLower(); if (!_LookupValues.ContainsKey(key)) _LookupValues.Add(key, value);
      key = enumInformation.WithSpaces.ToLower(); if (!_LookupValues.ContainsKey(key)) _LookupValues.Add(key, value);
    }

    _List.Sort((x, y) => x.Order.CompareTo(y.Order));
  }
  #endregion

  #region RemoveTitleCase
  private static string RemoveTitleCase(string value)
  {
    string result = value;

    result = System.Text.RegularExpressions.Regex.Replace(result, @"(?<begin>(\w*?))(?<end>[A-Z]+)", string.Format(@"${{begin}}{0}${{end}}", " ")).Trim();

    return result;
  }
  #endregion

  #region Description
  public static string Description(T value)
  {
    return Description(value, true);
  }
  public static string Description(T value, bool addSpaceBetweenWords)
  {
    string result = string.Empty;

    if (_Descriptions.ContainsKey(value)) result = (addSpaceBetweenWords ? _Descriptions[value].WithSpaces : _Descriptions[value].DefaultDescription);

    return result;
  }
  #endregion

  #region Count
  public static int Count()
  {
    return _Descriptions.Count;
  }
  #endregion

  #region Parse
  public static T Parse(string value)
  {
    T result;

    value = (value ?? string.Empty).ToLower();

    if (_LookupValues.ContainsKey(value))
    {
      result = _LookupValues[value];
    }
    else
    {
      throw new EnumParseException(string.Format("No Match for \"{0}\" in \"{1}\"", value, typeof(T).Name));
    }

    return result;
  }

  public static T Parse(string value, T defaultValue)
  {
    T result;

    value = (value ?? string.Empty).ToLower();

    if (_LookupValues.ContainsKey(value))
      result = _LookupValues[value];
    else
      result = defaultValue;

    return result;
  }

  public static bool TryParse(string value, out T parsedValue)
  {
    bool result = false;

    value = (value ?? string.Empty).ToLower();

    if (_LookupValues.ContainsKey(value))
    {
      parsedValue = _LookupValues[value];
      result = true;
    }
    else
    {
      parsedValue = default(T);
    }

    return result;
  }
  #endregion

  #region ToList
  public static List<T> ToList()
  {
    return ToList(false);
  }

  public static List<T> ToList(bool activeOnly)
  {
    List<T> result = new List<T>();

    foreach (EnumInformation enumInformation in _List)
    {
      if (!activeOnly || enumInformation.Active) result.Add(enumInformation.Value);
    }

    return result;
  }
  #endregion

  #region ToStringList
  public static List<string> ToStringList()
  {
    return ToStringList(true);
  }

  public static List<string> ToStringList(bool addSpaceBetweenWords)
  {
    return ToStringList(addSpaceBetweenWords, false);
  }

  public static List<string> ToStringList(bool addSpaceBetweenWords, bool activeOnly)
  {
    List<string> result = new List<string>();

    foreach (EnumInformation enumInformation in _List)
    {
      if (!activeOnly || enumInformation.Active) result.Add((addSpaceBetweenWords ? enumInformation.WithSpaces : enumInformation.DefaultDescription));
    }

    return result;
  }
  #endregion

  #region ToStringDictionary
  public static Dictionary<string, string> ToStringDictionary()
  {
    return ToStringDictionary(true);
  }

  public static Dictionary<string, string> ToStringDictionary(bool addSpaceBetweenWords)
  {
    return ToStringDictionary(addSpaceBetweenWords, false);
  }

  public static Dictionary<string, string> ToStringDictionary(bool addSpaceBetweenWords, bool useDescriptionAsKey)
  {
    return ToStringDictionary(addSpaceBetweenWords, useDescriptionAsKey, false);
  }

  public static Dictionary<string, string> ToStringDictionary(bool addSpaceBetweenWords, bool useDescriptionAsKey, bool activeOnly)
  {
    Dictionary<string, string> result = new Dictionary<string, string>();

    foreach (EnumInformation enumInformation in _List)
    {
      string description = (addSpaceBetweenWords ? enumInformation.WithSpaces : enumInformation.DefaultDescription);
      string key = (useDescriptionAsKey ? description : enumInformation.Value.ToString());

      if (!activeOnly || enumInformation.Active) result.Add(key, description);
    }

    return result;
  }
  #endregion

  #region ToDictionary
  public static Dictionary<int, string> ToDictionary()
  {
    return ToDictionary(true);
  }

  public static Dictionary<int, string> ToDictionary(bool addSpaceBetweenWords)
  {
    return ToDictionary(addSpaceBetweenWords, false);
  }

  public static Dictionary<int, string> ToDictionary(bool addSpaceBetweenWords, bool activeOnly)
  {
    Dictionary<int, string> result = new Dictionary<int, string>();

    foreach (EnumInformation enumInformation in _List)
    {
      if (!activeOnly || enumInformation.Active) result.Add(Convert.ToInt32(enumInformation.Value), (addSpaceBetweenWords ? enumInformation.WithSpaces : enumInformation.DefaultDescription));
    }

    return result;
  }
  #endregion

  #region ToValueDictionary
  public static Dictionary<T, string> ToValueDictionary()
  {
    return ToValueDictionary(true);
  }

  public static Dictionary<T, string> ToValueDictionary(bool addSpaceBetweenWords)
  {
    return ToValueDictionary(addSpaceBetweenWords, false);
  }

  public static Dictionary<T, string> ToValueDictionary(bool addSpaceBetweenWords, bool activeOnly)
  {
    Dictionary<T, string> result = new Dictionary<T, string>();

    foreach (EnumInformation enumInformation in _List)
    {
      if (!activeOnly || enumInformation.Active) result.Add(enumInformation.Value, (addSpaceBetweenWords ? enumInformation.WithSpaces : enumInformation.DefaultDescription));
    }

    return result;
  }
  #endregion
}
Published 10/22/2009 by Ron Muth


How to use an ASP.NET SiteMapProvider to produce a XML SiteMap for Google, Yahoo, Bing, etc

More often I’m being asked to provide “SiteMap.xml”s for sites we work on, and up until recently we had been hand rolling the xml and placing them in static documents on our sites. This is fine but seemed a little silly because ASP.NET has a built in SiteMap mechanism. So we decided to do something about using it for auto generating the SiteMap.xml.

My example is using Maarten Balliauw’s awesome MvcSiteMap available at http://mvcsitemap.codeplex.com/ but the conversion code works perfectly fine with the built in SiteMapProvider in ASP.NET.

Using the following class we can generate the XML object representing the SiteMap.

  public class SiteMapXMLNode
  {
    public enum enChangeFrequency
    {
      NoneGiven,
      Always,
      Hourly,
      Daily,
      Weekly,
      Monthly,
      Yearly,
      Never
    }

    [System.Xml.Serialization.XmlElement(ElementName = "loc")]
    public string URL;

    #region Last Modified
    [System.Xml.Serialization.XmlIgnore]
    public DateTime LastModified;
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    [System.Xml.Serialization.XmlElement(ElementName = "lastmod")]
    public string __LastModified
    {
      get { return (LastModified == DateTime.MinValue ? null : string.Format("{0:yyyy-MM-dd}", LastModified)); }
      set { LastModified = DateTime.MinValue; } //Note: This property will never be deserialized
    }
    #endregion

    #region Change Frequency
    [System.Xml.Serialization.XmlIgnore]
    public enChangeFrequency ChangeFrequency;
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    [System.Xml.Serialization.XmlElement(ElementName = "changefreq")]
    public string __ChangeFrequency
    {
      get { return (ChangeFrequency == enChangeFrequency.NoneGiven ? null : ChangeFrequency.ToString().ToLower()); }
      set { ChangeFrequency = enChangeFrequency.NoneGiven; } //Note: This property will never be deserialized
    }
    #endregion

    #region Priority
    [System.Xml.Serialization.XmlIgnore]
    public decimal? Priority;
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    [System.Xml.Serialization.XmlElement(ElementName = "priority")]
    public string __Priority
    {
      get { return (!Priority.HasValue ? null : string.Format("{0:0.0}", Priority)); }
      set { Priority = null; } //Note: This property will never be deserialized
    }
    #endregion

    public SiteMapXMLNode()
    {
      URL = null;
      LastModified = DateTime.MinValue;
      ChangeFrequency = enChangeFrequency.NoneGiven;
      Priority = null;
    }

    public SiteMapXMLNode(System.Web.SiteMapNode node)
      : this()
    {
      UriBuilder uriBuilder = new UriBuilder(System.Web.HttpContext.Current.Request.Url);
      uriBuilder.Path = node.Url;
      URL = uriBuilder.ToString();
    }
  }

  [System.Xml.Serialization.XmlRootAttribute(Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9", IsNullable = false, ElementName = "urlset")]
  public class SiteMapXML
  {
    private List<SiteMapXMLNode> Nodes;

    #region "urls"
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    [System.Xml.Serialization.XmlElementAttribute("url")]
    public SiteMapXMLNode[] __Nodes
    {
      get
      {
        return Nodes.ToArray();
      }
      set
      {
        Nodes.Clear();
        Nodes.AddRange(value);
      }
    }
    #endregion

    public SiteMapXML()
    {
      Nodes = new List<SiteMapXMLNode>();
    }

    public SiteMapXML(System.Web.SiteMapNode rootNode)
      : this()
    {
      Add(rootNode);
    }

    public void Add(System.Web.SiteMapNode rootNode)
    {
      Nodes.Add(new SiteMapXMLNode(rootNode));

      if (rootNode.HasChildNodes)
      {
        foreach (System.Web.SiteMapNode node in rootNode.ChildNodes)
        {
          if (node.IsAccessibleToUser(System.Web.HttpContext.Current)) Add(node);
        }
      }
    }
  }

Then we use this action for MVC

    public ActionResult SiteMapXML()
    {
      ActionResult result = null;

      SiteMapXML siteMapXML = new SiteMapXML(System.Web.SiteMap.RootNode);

      using (System.IO.MemoryStream oStream = new System.IO.MemoryStream())
      {
        System.Xml.Serialization.XmlSerializer xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(SiteMapXML));
        xmlSerializer.Serialize(oStream, siteMapXML);
        oStream.Flush();
        xmlSerializer = null;

        System.Text.UTF8Encoding UTF8 = new System.Text.UTF8Encoding(false);

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

      return result;
    }

Or this code behind for ASP.NET

    protected void Page_Load(object sender, EventArgs e)
    {
      Response.Clear();

      Response.ContentType = "text/xml";

      SiteMapXML siteMapXML = new SiteMapXML(System.Web.SiteMap.RootNode);

      System.Xml.Serialization.XmlSerializer xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(SiteMapXML));
      xmlSerializer.Serialize(Response.OutputStream, siteMapXML);
      xmlSerializer = null;

      Response.Flush();

      Response.End();
    }

It’s really that simple

 

ToDo: add Attributes and elements to the SiteMap definition so we can take advantage of the additional Attributes in the SiteMap XSD (“lastmod”, “changefreq” and “priority”)

Published 10/16/2009 by Ron Muth


NAnt task for Code Signing

At ISI we like to sign our assemblies for many reasons, but chief among them is so that we know and can prove that we compiled the assembly. The vast majority of our clients purchase the source code to the applications we work on for them, thus, from time to time, we’ve had problems where a deployed app doesn’t work but the assemblies we delivered work perfectly, and later find out that some one on the client side made a “minor” code change and deployed their own assemblies ……

Thus comes in one of the great reasons for code signing ……

We are also big fans of NAnt for builds, so below is the code to our NAnt task for signing assemblies:

using System;
using System.Collections.Generic;
using System.Text;

namespace ISI.NAnt.Contrib.Tasks
{
  [global::NAnt.Core.Attributes.TaskName("ISI.SignTool")]
  public class SignTool : global::NAnt.Core.Tasks.ExternalProgramBase
  {
    [global::NAnt.Core.Attributes.BuildElement("Assemblies", Required = true)]
    public virtual global::NAnt.Core.Types.FileSet Assemblies { get; set; }

    [global::NAnt.Core.Attributes.TaskAttribute("CertificateFileName", Required = true)]
    public string CertificateFileName { get; set; }

    [global::NAnt.Core.Attributes.TaskAttribute("CertificatePasswordFileName", Required = true)]
    public string CertificatePasswordFileName { get; set; }

    [global::NAnt.Core.Attributes.TaskAttribute("TimeServerUrl", Required = false)]
    public string TimeServerUrl { get; set; }

    public SignTool()
    {
      using (Microsoft.Win32.RegistryKey regKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Microsoft SDKs\Windows"))
      {
       if (regKey != null)
        {
          string installRoot = regKey.GetValue("CurrentInstallFolder").ToString();
          if (!string.IsNullOrEmpty(installRoot)) base.ExeName = System.IO.Path.Combine(installRoot, @"bin\signtool.exe");
        }
      }

      TimeServerUrl = @"http://timestamp.comodoca.com/authenticode";
    }

    public override string ProgramArguments
    {
      get { return string.Empty; }
    }

    protected override void ExecuteTask()
    {
      bool doSignCode = true;

      Log(global::NAnt.Core.Level.Info, "Code Sign");

      if (string.IsNullOrEmpty(CertificateFileName) || !System.IO.File.Exists(CertificateFileName))
      {
        Log(global::NAnt.Core.Level.Error, "Missing Certificate: {0}", CertificateFileName);
        doSignCode = false;
      }

      if (string.IsNullOrEmpty(CertificatePasswordFileName) || !System.IO.File.Exists(CertificatePasswordFileName))
      {
        Log(global::NAnt.Core.Level.Error, "Missing Certificate Password: {0}", CertificatePasswordFileName);
        doSignCode = false;
      }

      if (string.IsNullOrEmpty(base.ExeName) || !System.IO.File.Exists(base.ExeName))
      {
        Log(global::NAnt.Core.Level.Error, "Missing Sign Tool: {0}", base.ExeName);
        doSignCode = false;
      }

      if (doSignCode)
      {
        string certificatePassword = null;
        using (System.IO.StreamReader streamReader = new System.IO.StreamReader(CertificatePasswordFileName)) certificatePassword = streamReader.ReadLine();

        base.Arguments.Clear();
        base.Arguments.Add(new global::NAnt.Core.Types.Argument("sign"));
        base.Arguments.Add(new global::NAnt.Core.Types.Argument(string.Format("/f \"{0}\"", CertificateFileName)));
        base.Arguments.Add(new global::NAnt.Core.Types.Argument(string.Format("/p \"{0}\"", certificatePassword)));
        base.Arguments.Add(new global::NAnt.Core.Types.Argument(string.Format("/t \"{0}\"", TimeServerUrl)));

        doSignCode = false;
        foreach (string assemblyFileName in Assemblies.FileNames)
        {
          Log(global::NAnt.Core.Level.Info, "  Signing '{0}'.", assemblyFileName);

          global::NAnt.Core.Types.Argument fileArgument = new global::NAnt.Core.Types.Argument();
          base.Arguments.Add(fileArgument);
          fileArgument.File = new System.IO.FileInfo(assemblyFileName);

          doSignCode = true;
        }

        if (doSignCode)
          base.ExecuteTask();
        else
          Log(global::NAnt.Core.Level.Error, "No Files Found");
      }
    }
  }
}

 

and add it to your build script:

    <loadtasks assembly="/path_to/ISI.NAnt.Contrib.dll" />
    <ISI.SignTool CertificateFileName="/path_to/Certificate.pfx"
                  CertificatePasswordFileName="/path_to/Certificate.pwd"
      >
      <Assemblies basedir="${publish.dir}">
        <include name="ISI.*.dll" />
        <include name="ISI.*.exe" />
      </Assemblies>
    </ISI.SignTool>

Certificate.pfx is the code signing certificate
Certificate.pwd is the plain text password for that certificate

If these files do not exist, the SignTool task just skips itself

Note, the Certificate.pfx and Certificate.pwd files only exists on our official release build system.

Published 06/19/2009 by Ron Muth


Multiple configuration section for different developers

Building on my last post Custom Configuration section….

We often have multiple developers working on the same project but want to be running under different configurations. We do this by adding their machine name to custom config section and replacing the static constructor of the custom config section:

    static Configuration()
    {
      const string ConfigurationSectionName = "ISI.HAPP.API.Configuration";

      Current = null;
      if (Current == null) Current = (Configuration)(System.Configuration.ConfigurationManager.GetSection(string.Format("{0}.{1}", ConfigurationSectionName, System.Environment.MachineName.ToLower())));
      if (Current == null) Current = (Configuration)(System.Configuration.ConfigurationManager.GetSection(ConfigurationSectionName));
      if (Current == null) throw new Exception(string.Format("Missing {0} Configuration Section", ConfigurationSectionName));
    }

and adding a new config section in the app/web.config:

  <ISI.HAPP.API.Configuration.ronmuth>
    <CompanyName>ISI Ron Muth</CompanyName>
    <Authenication>
      <UseNTPermissions>true</UseNTPermissions>
      <DefaultNTDomain>ISI</DefaultNTDomain>
    </Authenication>
  </ISI.HAPP.API.Configuration.ronmuth>
Published 06/19/2009 by Ron Muth


Custom Configuration section

I HATE “ConfigurationSettings.AppSettings” !!!!

That being said, .NET gives us a great alternative that no one talks about, custom configuration sections. It gives us a fully nestable and strongly typed config section.

Add a new class:

using System;
using System.Configuration;

namespace ISI.HAPP.API
{
  public sealed partial class Configuration : ConfigurationSection
  {
    public class AuthenicationParameters : ConfigurationElement
    {
      #region UseNTPermissions
      /// <summary>
      /// Gets or sets the UseNTPermissions.
      /// </summary>
      [ConfigurationProperty("UseNTPermissions", IsRequired = true)]
      public bool UseNTPermissions
      {
        get
        {
          return (bool)base["UseNTPermissions"];
        }
        set
        {
          base["UseNTPermissions"] = value;
        }
      }
      #endregion

      #region DefaultNTDomain
      /// <summary>
      /// Gets or sets the DefaultNTDomain.
      /// </summary>
      [ConfigurationProperty("DefaultNTDomain", IsRequired = false)]
      public string DefaultNTDomain
      {
        get
        {
          return (string)base["DefaultNTDomain"];
        }
        set
        {
          base["DefaultNTDomain"] = value;
        }
      }
      #endregion
    }

    #region Current
    public static readonly Configuration Current;
    static Configuration()
    {
      const string ConfigurationSectionName = "ISI.HAPP.API.Configuration";

      Current = null;
      if (Current == null) Current = (Configuration)(System.Configuration.ConfigurationManager.GetSection(ConfigurationSectionName));
      if (Current == null) throw new Exception(string.Format("Missing {0} Configuration Section", ConfigurationSectionName));
    }
    #endregion

    public Configuration() : base() { }

    #region CompanyName
    [ConfigurationProperty("CompanyName", IsRequired = false)]     public string CompanyName
    {
      get
      {
        return (string)base["CompanyName"];
      }
      set
      {
        base["CompanyName"] = value;
      }
    }
    #endregion

    #region Authenication
    public AuthenicationParameters Authenication
    {
      get
      {
        return (AuthenicationParameters)base["Authenication"];
      }
    }
    #endregion
  }
}

and add the following to the “configSections” of your app/web.config:

    <section name="ISI.HAPP.API.Configuration" type="ISI.HAPP.API.Configuration, ISI.HAPP.API.Core" requirePermission="false"/>

and the following after the “configSections”

  <ISI.HAPP.API.Configuration>
    <CompanyName>ISI</CompanyName>
    <Authenication>
      <UseNTPermissions>true</UseNTPermissions>
      <DefaultNTDomain>ISI</DefaultNTDomain>
    </Authenication>
  </ISI.HAPP.API.Configuration>

 

It’s that simple …….

Published 06/19/2009 by Ron Muth


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 by Ron Muth