Search code examples
asp.net-coreasp.net-core-mvcasp.net-core-3.1strong-typing

Type-safe `a` tag helper


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:

enter image description here

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

Type-safe reference to action

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)"

Type-safe reference to controller

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.

Type-safe route parameters

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 }))

Full type-safe example

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?


Solution

  • 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:

    https://youtu.be/cFD9QOjEIxc