DEV Community

loading...
Cover image for ASP.NET Core Endpoints. Add endpoint-enabled middleware by using IEndpointRouteBuilder extension method

ASP.NET Core Endpoints. Add endpoint-enabled middleware by using IEndpointRouteBuilder extension method

nikiforovall profile image Oleksii Nikiforov ・5 min read

TL;DR

You can use extension method (e.g. IEndpointConventionBuilder Map{FeatureToMap}(this IEndpointRouteBuilder endpoints) to add a middleware to specific endpoint.


Middleware

Middleware forms the basic building blocks of the HTTP Pipeline. It is a really good concept to implement cross-cutting concerns and weave a re-usable piece of code into the ASP.NET pipeline. Middleware provides application-level features. For example, you might need Middleware to implement features like Routing, Cookies, Session, CORS, Authentication, HTTPS Redirection, Caching, Response Compression, Exception Handling. Most of the time, you've got out-of-the-box option provided by the framework.

To extend ASP.NET Core pipeline we use IApplicationBuilder injected in Startup.cs.

Furthermore, since ASP.NET Core 3.0 you could use HttpContext.GetEndpoint to retrieve selected endpoint/metadata, e.g.:

// Before routing runs, endpoint is always null here
app.Use(next => context =>
{
    Console.WriteLine($"Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseRouting();

// After routing runs, endpoint will be non-null if routing found a match
app.Use(next => context =>
{
    Console.WriteLine($"Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});
Enter fullscreen mode Exit fullscreen mode

Middleware can exist as simple inline methods or as complex, reusable classes. If you don't like RequestDelegate notation/form. You can use IApplicationBuilder.UseMiddleware extension method to add middleware as a class and access an endpoint from parameter (HttpContext).

// Startup.cs
app.UseMiddleware<MyAwesomeMiddleware>();
// ...
// MyAwesomeMiddleware.cs
public async Task InvokeAsync(HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
}
Enter fullscreen mode Exit fullscreen mode

If you need something more fine-grained and, probably, more powerful you can use Endpoint concept.

Routing and Endpoints

Routing is responsible for matching incoming HTTP requests and dispatching those requests to the app's executable endpoints. You can use IApplicationBuilder.UseEndpoints to define pipeline logic based on a selected route. Many of ASP.NET Core features/aspects are implemented with the routing concept in mind. For example, you can IEndpointRouteBuilder.MapControllers or IEndpointRouteBuilder.MapGrpcService or you can build your own framework based on this extension capabilities.

app.UseEndpoints(endpoints =>
{
    // Configure another endpoint, with authorization.
    endpoints.MapGet("/", async context =>
    {
        await context.Response.WriteAsync($"Hello from {context.GetEndpoint()}!");
    }).RequireAuthorization().WithMetadata(new AuditPolicyAttribute(needsAudit: true));
});
Enter fullscreen mode Exit fullscreen mode

As you can see, Endpoint contains an EndpointMetadataCollection that holds various data put and managed during pipeline execution.

/// <summary>
/// Represents a logical endpoint in an application.
/// </summary>
public class Endpoint
{
    public Endpoint(
        RequestDelegate? requestDelegate,
        EndpointMetadataCollection? metadata,
        string? displayName)
    {
        RequestDelegate = requestDelegate;
        Metadata = metadata ?? EndpointMetadataCollection.Empty;
        DisplayName = displayName;
    }
    /// <summary>
    /// Gets the informational display name of this endpoint.
    /// </summary>
    public string? DisplayName { get; }
    /// <summary>
    /// Gets the collection of metadata associated with this endpoint.
    /// </summary>
    public EndpointMetadataCollection Metadata { get; }
    /// <summary>
    /// Gets the delegate used to process requests for the endpoint.
    /// </summary>
    public RequestDelegate? RequestDelegate { get; }
}
Enter fullscreen mode Exit fullscreen mode

The big picture:

Alt Text

Practical Example

Recently, I've stumbled upon IConfigurationRoot.GetDebugView. Basically, it allows you to dump configuration values and reason about how configuration settings are resolved. Personally, I find this a really useful and productive way of reading the component configuration. You can enable a special endpoint to check your configuration by adding the following code to your UseEndpoints method (as part of Startup.cs).

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/config", async context =>
    {
        var config = (Configuration as IConfigurationRoot).GetDebugView();
        await context.Response.WriteAsync(config);
    });
});
Enter fullscreen mode Exit fullscreen mode

Here is an example output:

Alt Text

NikiforovAll.ConfigurationDebugViewEndpoint

The example above works and everything, but I took a chance and wrapped this functionality into NuGet package - https://github.com/NikiforovAll/ConfigurationDebugViewEndpoint.

It is an example of how to organize code base and apply the technique to make your Startup.cs more readable and composable. I will guide you through the process.

Our goal is to have an extension method that will allow us to plug /config endpoint. Something like this:

app.UseEndpoints(endpoints =>
{
    endpoints.MapConfigurationDebugView("/config", (options) => options.AllowDevelopmentOnly = true);
});
Enter fullscreen mode Exit fullscreen mode

Let's start with the ConfigurationDebugViewMiddleware. We want to write IConfiguration debug view output to Response.

public class ConfigurationDebugViewMiddleware
{
    public ConfigurationDebugViewMiddleware(RequestDelegate next) { }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        var configuration = httpContext.RequestServices.GetService<IConfiguration>();
        var config = (configuration as IConfigurationRoot).GetDebugView();
        await httpContext.Response.WriteAsync(config);
    }
}
Enter fullscreen mode Exit fullscreen mode

The trick is to create EndpointRouteBuilderExtensions.cs and write a small extension method that will allow us to plug ConfigurationDebugViewMiddleware.

Generally, we follow the next approach/signature

IEndpointConventionBuilder Map{FeatureToMap}(this IEndpointRouteBuilder endpoints, string pattern = <defaultPattern>, Action<{FeatureOptions}> configure);
Enter fullscreen mode Exit fullscreen mode

The actual implementation is based on the fact that you can create a sub-pipeline using the same abstraction - IApplicationBuilder that you use for your Startup.cs pipeline. IEndpointRouteBuilder.CreateApplicationBuilder creates a IApplicationBuilder that you can use and configure.

For example, here is a definition of an imaginary pipeline that handles SOAP requests:

var pipeline = endpoints.CreateApplicationBuilder()
    .UseMiddleware<LoggingMiddleware>()
    .UseMiddleware<CachingMiddleware>()
    .UseMiddleware<SOAPEndpointMiddleware>()
    .Build();

return endpoints.Map(pattern, pipeline)
Enter fullscreen mode Exit fullscreen mode

Complete implementation:

/// <summary>
/// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add routes.
/// </summary>
public static class EndpointRouteBuilderExtensions
{
    /// <summary>
    /// Adds a configuration endpoint to the <see cref="IEndpointRouteBuilder"/> with the specified template.
    /// </summary>
    /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add endpoint to.</param>
    /// <param name="pattern">The URL pattern of the endpoint.</param>
    /// <param name="optionsDelegate"></param>
    /// <returns>A route for the endpoint.</returns>
    public static IEndpointConventionBuilder? MapConfigurationDebugView(
        this IEndpointRouteBuilder endpoints,
        string pattern = "config",
        Action<ConfigurationDebugViewOptions>? optionsDelegate = default)
    {
        if (endpoints == null)
        {
            throw new ArgumentNullException(nameof(endpoints));
        }

        var options = new ConfigurationDebugViewOptions();
        optionsDelegate?.Invoke(options);

        return MapConfigurationDebugViewCore(endpoints, pattern, options);
    }

    private static IEndpointConventionBuilder? MapConfigurationDebugViewCore(IEndpointRouteBuilder endpoints, string pattern, ConfigurationDebugViewOptions options)
    {
        var environment = endpoints.ServiceProvider.GetRequiredService<IHostEnvironment>();
        var builder = endpoints.CreateApplicationBuilder();

        if (options.AllowDevelopmentOnly && !environment.IsDevelopment())
        {
            return null;
        }
        var pipeline = builder
            .UseMiddleware<ConfigurationDebugViewMiddleware>()
            .Build();

        return endpoints.Map(pattern, pipeline);
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

I've shared with you a simple but yet useful technique to organize your codebase. It allows you to keep your endpoint-related code nice and clean, developers can easily find extensions by conventions. Do you have other best practices to organize Startup.cs? Feel free to list them in the comments, I would like to hear from you!

You might want to try the configuration-debug-view NuGet package. https://github.com/NikiforovAll/ConfigurationDebugViewEndpoint.

Original blogpost: https://nikiforovall.github.io/dotnet/aspnetcore/2021/03/23/endpoint-route-builder-extension-pattern.html

Reference

Discussion (0)

pic
Editor guide