Routing is the backbone of every web application, and with ASP.NET Core, it’s never been easier to mix and match request handling frameworks. We can choose from MVC, Blazor, Razor Pages, SignlaR, Carter, and many more. Arguably the strength of ASP.NET Core is the interoperability of all of these approaches within a single host environment.

In this post, we’ll see how we can name our Map methods so that we can generate links to the endpoints from anywhere within our ASP.NET Core application.

Why Use The Map Approach?

With .NET 5, Microsoft has decoupled routing from the MVC and WebAPI frameworks. This change allows developers to access information about the current request in new locations that were previously unavailable. As of the latest version of ASP.NET Core, we can comprise all our web applications using endpoints, but what is an endpoint?

An endpoint is a data structure that can be: Selected matching the URL and HTTP method. Executed by running a method delegate.

Let’s take a look at an example. Within our Configure method, we can call several Map extension methods.

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", async context =>
    {
        await context.Response.WriteAsync("Hello, World!");
    });
});

We can also map endpoints for frameworks like MVC, Blazor, and SignalR by calling the respective methods of MapControllers, MapBlazorHub, and MapHub<>.

ASP.NET Core has several low-level mapping methods that match HTTP methods: MapGet, MapPost, MapPut, and MapDelete. These extension methods are shorthand for the Map method, along with some default metadata.

public static IEndpointConventionBuilder MapMethods(
   this IEndpointRouteBuilder endpoints,
   string pattern,
   IEnumerable<string> httpMethods,
   RequestDelegate requestDelegate)
{
    if (httpMethods == null)
    {
        throw new ArgumentNullException(nameof(httpMethods));
    }

    var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate);
    builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}");
    builder.WithMetadata(new HttpMethodMetadata(httpMethods));
    return builder;
}

The advantage of using these Map methods is that we can opt-in to functionality, thus allowing us to fine-tune our web application’s performance profile. This is especially helpful when we know our exact use-case. For example, we may know we’ll be building a JSON only HTTP endpoint. We can increase performance by excluding code for XML content negotiation and unnecessary calls to pipeline steps.

Now that we have a basic understanding of endpoints let’s talk about routes and utilize path definitions to generate links to other endpoints.

As mentioned in the previous section, most paths into our ASP.NET Core application can be considered an endpoint. The strength of building a web application is our ability to link to other endpoints. Luckily for us, when using endpoint routing in ASP.NET Core, we also have access to the LinkGenerator class.

The LinkGenerator class gives us the ability to generate absolute and related URIs based on previously registered endpoints. The LinkGenerator is a critical feature that helps us avoid mistakes when generating links.

To get the most out of our endpoints, we’ll want to register our endpoints with names. We do this by using the fluent interface method of WithMetadata after declaring our endpoint. To this method, we’ll pass a new instance of EndpointNameMetadata.

    // 1. Register an endpoint
    //    as you normally would
    endpoints.MapGet("hello/{name}", async context =>
    {
        var name = context.GetRouteValue("name") ?? "world";
        await context.Response.WriteAsync($"Hello {name}!");
    })
    // 2. After the declaration you need
    //    to add an `EndpointNameMetadata` to the
    //    endpoint declaration
    .WithMetadata(new EndpointNameMetadata("hello-name"));

To generate a link to this endpoint, we only need to refer to it by its new name. In this case, the name of our endpoint is hello-name.

    endpoints.MapGet("/", async context =>
    {
        // 3. Pull the `LinkGenerator` from the
        //    DI container, you'll need this
        var links = context.RequestServices.GetRequiredService<LinkGenerator>();
        // 4. Generate the link with the name
        //    from above, along with route values.
        var href = links.GetUriByName(
            context, 
            "hello-name", 
            new {name = "khalid"}
        );
        
        context.Response.ContentType = "text/html";
        // 5. Use the generated link in the code
        await context.Response.WriteAsync($"<a href=\"{href}\">Hello?!</a>");
    });

We can now link to this endpoint from anywhere within our ASP.NET Core application. This endpoint is now available by name from MVC, Blazor, Razor Pages, Middleware, and other endpoints. In the code featured above, we utilize the GetUriByName method. This allows us to get an absolute link to our endpoint, but we could also utilize GetPathByName which would give us a relative link.

Here is the complete solution inside our UseEndpoints method found in Startup.

app.UseEndpoints(endpoints =>
{
    // 1. Register an endpoint
    //    as you normally would
    endpoints.MapGet("hello/{name}", async context =>
    {
        var name = context.GetRouteValue("name") ?? "world";
        await context.Response.WriteAsync($"Hello {name}!");
    })
    // 2. After the declaration you need
    //    to add an `EndpointNameMetadata` to the
    //    endpoint declaration
    .WithMetadata(new EndpointNameMetadata("hello-name"));
    
    endpoints.MapGet("/", async context =>
    {
        // 3. Pull the `LinkGenerator` from the
        //    DI container, you'll need this
        var links = context.RequestServices.GetRequiredService<LinkGenerator>();
        // 4. Generate the link with the name
        //    from above, along with route values.
        var href = links.GetUriByName(
            context, 
            "hello-name", 
            new {name = "khalid"}
        );
        
        context.Response.ContentType = "text/html";
        // 5. Use the generated link in the code
        await context.Response.WriteAsync($"<a href=\"{href}\">Hello?!</a>");
    });
});

If you found this information helpful, feel free to share it on Twitter so other ASP.NET developers might also know this awesome knowledge.

We could also make this a tiny bit nicer by adding making the interface for adding names to our endpoints less verbose using an extension method.

public static class EndpointExtensions
{
    public static IEndpointConventionBuilder WithName(
        this IEndpointConventionBuilder builder, string name
    ) => builder.WithMetadata(new EndpointNameMetadata(name));
}

The extension method results in a small but appreciated change in our registration code.

// 1. Register an endpoint
//    as you normally would
endpoints.MapGet("hello/{name}", async context =>
{
    var name = context.GetRouteValue("name") ?? "world";
    await context.Response.WriteAsync($"Hello {name}!");
})
// 2. After the declaration you need
//    to add an `EndpointNameMetadata` to the
//    endpoint declaration
.WithName("hello-name");

Conclusion

Endpoints are a fantastic feature of ASP.NET Core. Using the Map methods can help us fine-tune a performant application profile and allow us to shed the weight of heavy frameworks. While we may be aiming for a trimmed down experience, we still want to use features like link generation. By adding a straightforward call to WithMetadata along with a new EndpointNameMetadata instance, we can reference our endpoints like we would a controller action or razor page. Knowing that the WithMetadata method exists opens a world of possibilities to developers.

Thanks for reading, and let me know in the comments if you found this post helpful.