One of the most popular books on ASP.NET Core is Pro ASP.NET Core 3 by Adam Freeman.
In chapters 7-11, he builds an example application, SportsStore:
On the left, you'll notice the buttons for various product categories ("Chess", "Soccer", etc.). These links are generated by the following code:
@foreach (string category in Model) {
<a class="btn btn-block
@(category == ViewBag.SelectedCategory
? "btn-primary": "btn-outline-secondary")"
asp-action="Index" asp-controller="Home"
asp-route-category="@category"
asp-route-productPage="1">
@category
</a>
}
Views/Shared/Components/NavigationMenu/Default.cshtml
This part:
asp-action="Index"
refers to the Index
method in HomeController
. However, this bit of code is not type-safe; if we have a typo:
asp-action="IndexAbc"
the project still compiles.
OK, no worries, we can fix that:
asp-action="@nameof(HomeController.Index)"
There we go. Now a typo such as the following will give us an immediate error at edit time and the project will of course not compile:
asp-action="@nameof(HomeController.IndexAbc)"
The reference to the controller is also not type-safe:
asp-controller="Home"
Solving this one is more verbose, but is doable:
asp-controller="@Regex.Replace(nameof(HomeController), "Controller$", String.Empty)"
Again, now typos in the controller name are caught at compile time. If the controller is renamed, this will also catch that as well.
How about also working with route values in a type-safe manner? Here are the two parameters and values we're dealing with:
asp-route-category="@category"
asp-route-productPage="1"
We can't really use nameof
as in the previous cases; there's nothing to really use nameof
on.
Here's the Index
method signature:
public ViewResult Index(string category, int productPage = 1)
It's almost like we'd like to be able to say nameof(productPage)
but we don't have access to the productPage
parameter in our view component.
Let's take a step back...
The following code:
<a class="btn btn-block @(category == ViewBag.SelectedCategory ? "btn-primary" : "btn-outline-secondary")"
asp-action="Index"
asp-controller="Home"
asp-route-category="@category"
asp-route-productPage="1">
@category
</a>
ultimately gets expanded into something like this:
<a class="btn btn-block btn-outline-secondary" href="/Chess/Page1">
Chess
</a>
In particular, these lines:
asp-action="Index"
asp-controller="Home"
asp-route-category="@category"
asp-route-productPage="1"
get squashed down to just something like this:
href="/Chess/Page1"
ASP.NET Core allows us to perform this mapping programmatically using the method LinkGenerator.GetPathByAction
.
So for example, the following call:
_linkGenerator.GetPathByAction("Index", "Home", new { category = "Chess", productPage = 1 })
gives us the following:
"/Chess/Page1"
We're very close now. The challenge now is that the following:
new { category = "Chess", productPage = 1 }
is not type-safe. The following typos do not prevent the project from building:
new { categoryAbc = "Chess", productPageXyz = 1 }
Let's change the Index
method to receive an object as a parameter:
public ViewResult Index(IndexParameters parameters)
where IndexParameters
is:
public class IndexParameters
{
public string Category { get; set; }
public int ProductPage { get; set; } = 1;
}
Now we can pass the parameter values in a type-safe manner:
_linkGenerator.GetPathByAction("Index", "Home", new IndexParameters() { Category = category, ProductPage = 1 }))
OK so now we have arrived at the full type-safe a
tag:
<a class="btn btn-block @(category == ViewBag.SelectedCategory ? "btn-primary" : "btn-outline-secondary")"
href="@(
_linkGenerator.GetPathByAction(
nameof(HomeController.Index),
Regex.Replace(nameof(HomeController), "Controller$", String.Empty),
new IndexParameters()
{
Category = category,
ProductPage = 1
}))">
@category
</a>
I have added the following items to the view component file for this new version of the code:
@using SportsStore.Controllers
@using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Routing
@inject LinkGenerator _linkGenerator
My question is... is there a more concise way to get the same level of type-safety in the same code? Or do I really have to jump through all those hoops to make the code type-safe?
Thanks to user quentech on reddit who suggested the following project:
R4MVC is a Roslyn based code generator for ASP.NET MVC Core apps that creates strongly typed helpers that eliminate the use of literal strings in many places.
It appears that this project aims to solve some of the same issues I've demonstrated in the question.
Video demonstrating statically typed tag helpers provided by R4MVC: