Active Sitemaps - An MVC Manifesto, Part 2

This is the second in a series of articles describing a powerful design pattern that provides several application services in a convenient form while also encapsulating a high-level description of the application's functionality. The first installment gave an overview of the pattern as well as the motivation that brought me to it. In case you missed it, you can read it here: Active Sitemaps Part 1.

The first part of this pattern that came about was extending the idea of attribute routing. The motivation was that I wanted the syntax for creating links in Razor views to prompt me for any necessary parameters. With Html.ActionLink(), a particular link might require an integer for a parameter or maybe a string, but there's no way you would be able to tell that without actually going to the action method and checking it.

The solution that I came up with works well, but I do think it is the weakest part of the whole pattern. It requires that a new class be created for each Route (bit of functionality) in the application. You see, I want to divorce the specifics of the link implementation from the syntax of its generation. For example, when I need a link to the details of a product, in the process of being prompted for what is necessary to create the link I don't want the details of the link to be apparent. That is, I should be prompted for the product to which I am linking, not an integer or GUID id or a string url slug.

@* instead of *@
@Html.ActionLink("View Details", "Details", "Product", new { id = 7 })

@* or even *@
<a href="@ProductDetailRouteAttribute.BuildUrl(currentProduct.Id)">View Details</a>

@* I want *@
<a href="@ProductDetailRouteAttribute.BuildUrl(currentProduct)">View Details</a>

In order to achieve that goal, it pretty much requires that separate classes be created for each URL. That's the only way to get such customized link creation methods. So let's get to the real code!

We are going to start with the old pattern of an interface with an abstract class implementing that interface with concrete classes inheriting from the abstract class. It will seem like overkill at first to go to those lengths just for what we will see in this article, but there are at least three more features that we are going to add to this pattern, so this approach will pay off in the long run. First we'll look at the interface (which needs to inherit from IRouteTemplateProvider in order to be a routing attribute).

public interface ILogicalRouteTemplateProvider : IRouteTemplateProvider {
//nothing here, yet. . . stay tuned to future installments in this series!
}

And the abstract class - most of the code is required in order to be a routing attribute; the only new part is the method to create a RedirectResult, which will be useful in controller action methods.

public abstract class RouteTemplateBaseAttribute : Attribute, ILogicalRouteTemplateProvider {
public abstract string Template { get; }
public virtual int? Order { get; set; }
public virtual string Name { get; set; }

protected RedirectResult MakeRedirect(string url) { return new RedirectResult(url); }
}

Now for any given bit of functionality (Route) that the application needs, we would create a class that inherits from this abstract base class. Simplest would be routes that have no parameters, such as contact functionality:

public class ContactUsRouteAttribute : RouteTemplateBaseAttribute {
public override string Template => "contact-us";
public string BuildUrl() {
return "/contact-us";
}
public RedirectResult CreateRedirect() {
return MakeRedirect("/contact-us");
}
}

The Template property defines the URL that will be mapped to the desired functionality. The BuildUrl() method will be used in Razor views when a hyperlink is required, and the RedirectResult() method will be used in component action methods when needing to redirect the browser to this functionality. Routes that need to be parameterized would require input parameters to these methods.

public class ProductDetailRouteAttribute : RouteTemplateBaseAttribute {
public override string Template => "products/details/{id}";
public string BuildUrl(Product prod) {
return $"/products/details/{prod.Id}";
}
public RedirectResult CreateRedirect(Product prod) {
return MakeRedirect($"/products/details/{prod.Id}";
}
}

In the Template property, the {id} defines the key in RouteData where the value from the third URL segment will be stored. This means that the controller action method should have a parameter with that same name, for model binding to fill it in automatically. The BuildUrl() and CreateRedirect() methods now have a parameter, so intellisense can prompt for it when those methods are used - no more guesswork or passing in the wrong kind of data or omitting it entirely!

So to connect these URLs to controller action methods, simply apply them as an attribute. And when your SEO folks want you to change the URL pattern for just one little part of your application, go right ahead - the only code that needs to change is in the attribute class for that bit of functionality (Route).

public class ProductController {

[ProductDetailRoute]
public IActionResult Details(int? id) {
//do work here
}
}

The one part remaining is how to conveniently access these route attributes. Their methods were created as instance methods, so there will need to be an instance of each one that we want to use. However, they store no state so singletons should work well. To meet this need, as well as to lay the groundwork for the features in the next couple of articles in this series, I created a class that has an instance of each route attribute as a static property. This class will ultimately encapsulate a complete description of the application's functionality, so this class will be the Active Sitemap.

public class AppMap {
public static SiteHomeRouteAttribute SiteHomeRoute
= new SiteHomeRouteAttribute();
public static ContactUsRouteAttribute ContactUsRoute
= new ContactUsRouteAttribute();
public static AboutUsRouteAttribute AboutUsRoute
= new AboutUsRouteAttribute();

public static ProductListRouteAttribute ProductListRoute
= new ProductListRouteAttribute();
public static ProductDetailsRouteAttribute ProductDetailsRoute
= new ProductDetailsRouteAttribute();
// and so on . . .
}

Now all of the routes are readily available inside of Razor views whenever a hyperlink is needed, and intellisense will prompt you all the way through the process:

<nav>
<a href="@AppMap.AboutUsRoute.BuildUrl()">About Us</a>
<a href="@AppMap.ContactUsRoute.BuildUrl()">Contact Us</a>
<a href="@AppMap.ProductListRoute.BuildUrl()">Our Products</a>
</nav>

<ul>
@foreach (var oneProd in Model.Products) {
<li><a href="@AppMap.ProductDetailsRoute.BuildUrl(oneProd)">@oneProd.Name</a>
}
</ul>

And similar prompting will happen when you need to redirect the browser from within a controller action method:

[ContactUsRoute]
[HttpPost]
[ValidateAntiforgeryToken]
public IActionResult Contact(ContactUsModel model) {
//send contact email

//redirect to the main page
return AppMap.SiteHomeRoute.CreateRedirect();
}

We are almost through this initial release of the Active Sitemap pattern. I was about this far in the initial project where this was developed when it was decided that we wanted to use absolute URLs all throughout the application - both for hyperlinks and for image paths. Since we are already using the AppMap object all throughout the application, that would be a useful place to put a method that would convert a relative URL to an absolute URL. Then it could be used for image URLs within Razor views and also in each route attribute when generating URLs.

So this method will need to correctly use http vs. https (whichever the site is using) as well as correctly capture the domain name at both design-time and run-time (mysite.com vs. localhost:43055, for example). In order to do this an HttpContextAccessor is needed. Also, we're basically working with a singleton of the Active Sitemap object. We could set that up with .NET Core dependency injection, but then we would have to inject it into basically every view and controller when we really just want to access static methods of it - too much work!

So we need an HttpContextAccessor, but we don't want to use DI. The HttpContextAccessor is readily available during application bootstrapping, but the actual HttpContext is not available until we are processing a request. So we can capture the context accessor into a static property during application bootstrap and then use it the first time that we create an absolute URL.

public class AppMap {
// in addition to the static route attribute properties above. . .
private static IHttpContextAccessor _httpContextAccessor;
private static string _scheme = "";
private static string _host = "";
private static bool _isConfigured = false;

public static void Configure(IHttpContextAccessor accessor) {
_httpContextAccessor = accessor;
}

public static string MakeAbsolute(string relativeUrl) {
relativeUrl = relativeUrl.ToLower();
if (relativeUrl.StartsWith("http")) return relativeUrl;

if (!_isConfigured) {
var request = _httpContextAccessor.HttpContext.Request;
_scheme = request.Scheme;
_host = request.Host.Value;
_isConfigured = true;
}
return new Uri(new Uri(_scheme + "://" + _host), relativeUrl).ToString();
}
}

And then we need to configure the Active Sitemap in the application startup (in addition, MVC no longer needs any route patterns registered, so that configuration can be removed).

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
//other config

var httpContextAccessor = app.ApplicationServices
.GetRequiredService<IHttpContextAccessor>();
AppMap.Configure(httpContextAccessor);

app.UseMvc(); //no route patterns needed here
}

Finally, the route attributes can return absolute URLs when they are generated:

public class ContactUsRouteAttribute : RouteTemplateBaseAttribute {
public override string Template => "contact-us";
public string BuildUrl() {
return AppMap.MakeAbsolute("/contact-us");
}
public RedirectResult CreateRedirect() {
return MakeRedirect("/contact-us");
}
}

And Razor views can have absolute URLs for images.

<img src="@AppMap.MakeAbsolute("/images/logo.png")" alt="Our Company Logo" />

Well, that was a long drive! We are at the first stage of our Active Sitemap. The full code for this example is available on GitHub here - fork it and try it out! We have quite a bit more to add to this pattern, so come back later and check out the next articles in this series. Until then, may all your code be groovy!

Author Avatar
Share this: