A new breed of magic strings in ASP.NET MVC


One of the common patterns in Ruby on Rails is the use of hashes in place of large parameter lists.  Instead of hashes, which don’t exist in C#, ASP.NET MVC uses anonymous types for quite a few HTML generators on the view side.  For example, we might want to generate a strongly-typed URL for a specific controller action:

Url.Action<ProductsController>(x => x.Edit(null), new { productId = prodId })

Now, this is an example where our controller action uses some model binding magic to bind Guids to an actual Product.  We need to pass in route values to the helper method, which will eventually get translated into a RouteValueDictionary.

All special MVC terms aside, this is a trick used by the MVC team to simulate hashes.  Since the dictionary initializer syntax is quite verbose, with lots of angly-bracket cruft, anonymous types provide a similar effect to hashes.  However, don’t let the magic fool you.  Anonymous types used to create dictionaries are still dictionaries.

See that “productId” property in the anonymous object above?  That’s a magic string.  It’s used to match up to the controller parameter name.  Other anonymous types are used to add HTML attributes, supply route information, and others.  It’s subtle, as it doesn’t look like a string, it looks like an object.  There aren’t any quotes, it just looks like I’m creating an object.

However, it suffers from the same issues that magic strings have:

  1. They don’t react well to refactoring
  2. It’s rampant, pervasive duplication
  3. They’re easily broken with mispelling

If I change the name of the parameter in the Edit method, all code expecting the parameter name to be something specific will break.  This wouldn’t be so bad if I had to change just one location, but I need to fix all places that use that parameter (likely with a Replace All command).  I understand the desire to separate views from controllers, but I don’t want to go backwards and now have brittle views.

Luckily, there are some easy ways to solve this problem.

Option #1 – Just use the dictionary

One quick way to not use magical anonymous types is to just use the dictionary.  It’s a little more verbose, but at least we can apply the “Once and only once” rule to our dictionary keys – and store them in a constant or static field.  We still have use the dictionary initializer syntax:

Url.Action<ProductsController>(x => x.Edit(null), new RouteValueDictionary { { ProductsController.ProductId, prodId } })

The ProductId field is just a constant we declared on our ProductsController.  But at least it’s strongly-typed, refactoring-friendly and simple.  It is quite a bit bigger than our original method, however.

Option #2 – Strongly-typed parameter objects

Instead of an anonymous object, just use a real object.  The object represents the parameters of the action method:

public class EditProductParams
{
    public Guid product { get; set; }
}

The name of this property matches the name of our parameter in the controller.  In our ASPX, we’ll use this class instead of an anonymous one:

Url.Action<ProductsController>(x => x.Edit(null), new EditProductParams { product = prodId })

We solve the “Once and only once” problem…almost.  The parameter name is still duplicated on the controller action method and this new class.  I might define this class beside the controller to remind myself, but the parameter still shows up twice.

In other applications of the anonymous object, this option really wouldn’t work.  For things like Html.ActionLink, where an anonymous type is used to populate HTML attributes, the mere presence of these properties may cause some strange things to happen.  It works, but only partially.  If the designers of MVC wanted to create parameter objects, they probably would have.

Option #3 – Use Builders

We can use a Builder for the entire HTML, or just the RouteValueDictionary.  Either way, we use a fluent builder to incrementally build the HTML or parameters we want:

<%= Url.Action<ProductsController>(x => x.Edit(null, null), Params.Product(product).Customer(customer)) %>

Instead of hard-coded strings, we use a builder to chain together all the information needed to build the correct route values.  Our builder encapsulates all of the parameter logic in one place, so that we can chain together whatever parameters it needs.  It starts with the chain initiator:

public static class Params
{
    public static ParamBuilder Product(Product product)
    {
        var builder = new ParamBuilder();

        return builder.Product(product);
    }

    public static ParamBuilder Customer(Customer customer)
    {
        var builder = new ParamBuilder();

        return builder.Customer(customer);
    }
}

The actual building of the parameters is in our ParamBuilder class:

public class ParamBuilder
{
    private Product _product;
    private Customer _customer;

    public ParamBuilder Product(Product product)
    {
        _product = product;
        return this;
    }

    public ParamBuilder Customer(Customer customer)
    {
        _customer = customer;
        return this;
    }

Because each new parameter method returns “this”, we can chain together the same ParamBuilder instance, allowing the developer to build whatever common parameters needed.  Finally, we need to make sure our ParamBuilder can be converted to a RouteValueCollection.  We do this by supplying an implicit conversion operator:

public static implicit operator RouteValueDictionary(ParamBuilder paramBuilder)
{
    var values = new Dictionary<string, object>();

    if (paramBuilder._product != null)
    {
        values.Add("p", paramBuilder._product.Id);
    }

    if (paramBuilder._product != null)
    {
        values.Add("c", paramBuilder._customer.Id);
    }

    return new RouteValueDictionary(values);
}

The compiler will automatically call this implicit operator, so we don’t need any “BuildDictionary” or other creation method, the compiler does this for us.  This is just an example of a builder method; the possibilities are endless for a design of “something that creates a dictionary”.

Each of these approaches has their plusses and minuses, but all do the trick of eliminating that new breed of magic strings in ASP.NET MVC: the anonymous type.

Enabling success with opinionated architecture