Skip to content

Instantly share code, notes, and snippets.

@rynowak
Created June 7, 2018 19:33
Show Gist options
  • Save rynowak/174c46b31f52d9674b55bf27dbf8b19d to your computer and use it in GitHub Desktop.
Save rynowak/174c46b31f52d9674b55bf27dbf8b19d to your computer and use it in GitHub Desktop.
Route Value Invalidation

Summary

Routing has a URL generation that feature that tries to make it simple and terse to generate URLs to related endpoints. This involves allowing ambient route values to be used in URL generation to obviate the need to re-specify them. Route value invalidation is the set of rules that govern the cases where routing can and cannot use the ambient values.

Example (Conventional Routing)

public class BlogController : Controller
{
    // Assume we just matched the route "{controller=Home}/{action=Index}/{id?}
    // with the URL /Blog/Edit/17
    public ActionResult Edit(int id)
    {
        // values = { controller = Blog, action = Edit }, ambient values = { controller = Blog, action = 17, id = 17 }
        var link_to_edit = Url.Action(); // generates /Blog/Edit/17
        
        // values = { controller = Blog, action = Index }, ambient values = { controller = Blog, action = 17, id = 17 }
        var link_to_index = Url.Action("Index");  // generates /Blog
    }
}

Note the difference between these two cases. When we're linking to the same action, the value id = 17 is reused. When we're not linking to the same action, the value id = 17 is discarded.

From a user expectations point of view, this largely does what users expect and easy to explain. When you link to the same action then any additional route values can be reused; when you link to a different action, the additional route values are discarded.

Important before you continue, ask yourself if you accept the above statement. Is that true in your experience? Does that idea pass the Dan test? Does it seem good?

Example (Attribute Routing)

public class ProductsController : Controller
{
    [HttpGet("/products/{productname}")]
    public ActionResult Description (string productname) { ... }
}

public class InventoryController : Controller
{
    // Assume the URL /inventory/plungers/9999
    [HttpGet("/inventory/{productname}/{itemcount}")]
    public ActionResult Search(string productname, int itemcount)
    {
        // values = { controller = Inventory, action = Search }
        // ambient values = { controller = Inventory, action = Search, productname = plungers, itemcount = 9999 }
        var link_to_product_description = Url.Action("Description", "Products"); // generates /products/plungers
    }
}

Wait what? I just told you that when you link to a different action, that all of the 'extra' parameters are ignored. In this case the productname = plungers route value is reused even though I'm going somewhere else...

Explained

So what's really happening here? Well URL geneneration decides to accept values from the ambient values via an algorithm that operates on the route template being considered. The way that this works is historical, and kinda naive - I think we could do a good job updating this alorithm and ruleset that will fix most of the surprising behavior users encounter.

The relevant code is here: https://github.com/aspnet/Routing/blob/dev/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs#L66

A brief summary is that it reads the list of route parameters from left to right, and accepts parameters until it meets one of the stop conditions:

  • a value in the 'values' is different than the corresponding value ambient values
  • a parameter has an unsatisfied value

So why does this fall down for attribute routing? Well, because the 'values' that select the action are not parameters. This means that when you link to a different action in attribute routing, it's totally unknown to this algorithm

I think a small tweak here would make this behavior consistent and predictable, to basically get attribute routing to behave the way conventional routing does today in principle. We just need to consider the 'controller' and 'action values as well.

When you link to the same action then any additional route values can be reused; when you link to a different action, the additional route values are discarded. Does that sound good?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment