Active Sitemaps - An MVC Manifesto, Part 1
03.31.2018
As with most innovations, it all started with frustration. Don't get me wrong - I absolutely love ASP.NET Core MVC. But since the original MVC release, I have been enormously frustrated with the way that links and redirects are generated.
@* in a Razor view *@
@Html.ActionLink("View Details",
"Details", "Product", new { id = 7 })
// or in a controller action
return RedirectToAction("Details", "Product",
new { id = 7 });
See that? The action method to display product details requires a product id. And while there is flexible capability to supply that id when a link or a redirect is created, the fact is that the code creating the link or the redirect has to know that the parameter is required and also has to know what the parameter is called. Not only is there no intellisense prompting for this parameter, but there is nothing preventing the code from supplying an incorrect data type. When creating the link or the redirect, it would be helpful if the developer were prompted with the required syntax and data type, in the way that they would be prompted when invoking a method of an object instance.
That's where it all began. When creating a link to product detail functionality, for example, I wanted my IDE to prompt me that a Product object was required. But that got me thinking. The real issue is that there is no abstraction layer describing the application as a whole. Bear with me, because that statement requires a bit of unpacking.
Object-oriented programming uses abstraction layers to create loosely-coupled, yet strongly-typed, applications. This approach provides robust compile-time checking and development-time assistance while creating flexibility, providing testability, and easing long-term maintenance of the application.
MVC's implementation of links and redirects is both too tightly-coupled and too loosely-coupled at the same time. It's too tightly-coupled to a particular implementation of functionality (the specific action method and controller are specified), and it's too loosely-coupled to be able to inform the developer of the required parameters of that implementation.
My journey to solve this problem led down a long road that ended in a rather interesting pattern that solved several issues. It will likely take five or six articles here to describe it, but if you hang with me, you will pick up a design pattern that is quite powerful. You may choose to implement any part of it that you wish. . . there is a core infrastructure to it, but you don't have to apply all aspects that we will cover here.
From a big-picture standpoint, there will be a hierarchy of objects (I'm calling it an "Active Sitemap") that provide an abstract description of the entire application functionality. The individual objects are used as routing attributes on the desired action methods. This provides the mapping between the abstraction (the hierarchy of objects) and the implementation of that abstraction (the controller action methods). Therefore, a new implementation of a particular piece of application functionality can be rolled out simply by moving the corresponding attribute from the current method to the new one.
[ProductDetailsRoute]
public async Task<IActionResult> Details(int? id) {
//implementation code here
}
The objects in the Active Sitemap define the URL pattern that maps to each particular bit of functionality and are used to generate links and redirects. Since there is a different object for each bit of application functionality, they have individual methods for generating links and redirects, which can have parameters specific to that bit of functionality. Because these objects encapsulate that data and behavior, it is easy to change URLs and their patterns without having to make corresponding changes elsewhere in the application. For example, when your SEO experts want you to change your blog posting URLs to use a slugified title instead of an id, that is easily accomplished.
@* in a Razor view *@
<a href="@ProductDetailsRouteAttribute
.BuildUrl(currentProduct)" >View Details</a>
// in a controller action method
return ProductDetailsRouteAttribute
.CreateRedirect(currentProduct);
After I got that part working, it turned out to be a natural extension to use the hierarchy to dynamically generate the sitemap for the application. A custom piece of middleware in the pipeline can handle requests for the sitemap.xml file and dynamically generate it based on the hierarchy (making any necessary database calls and loops in the process).
It also turned out to be a small step to use that same hierarchy to automatically create breadcrumbs on the site. The Active Sitemap has a method that will build and output the html for the breadcrumb. It discovers which element in the hierarchy represents the current URL and then works its way back up the hierarchy, writing out the breadcrumb elements.
Lastly, the Active Sitemap became a natural place to apply security. After all, if you have an abstract description of the entire set of functionality for the application, that is the natural place to apply security restrictions. As a bonus, since that abstraction is expressed as objects, inheritance can be used to simplify the implementation. (Interesting side note: while building this part, I built in a switch to easily disable all security checks during development, so I could avoid the hassle of logging in all the time!)
And who knows? By the time the last of the articles in this series is written, I may come up with something else that this Active Sitemap can do for my applications.
So, this post was not intended to be a tease, but I felt it necessary to provide the motivation and an overview of where I'm headed with this series of articles. Stay tuned for part two, where I will start with the route attributes and link- and redirect-generation. In the meantime, may all your code be groovy!
You can read part two of this series here.