Skip to content

Best practice for a singleton route handler filter with Minimal APIs? #41259

Open
@martincostello

Description

@martincostello

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

The new support for route handler filters for Minimal APIs currently supports these use cases:

  1. Use an IRouteHandlerFilter instance specified when the route is mapped.
  2. Create a new instance of type T that implements IRouteHandlerFilter per HTTP request and invoke it. The filter is created with access to the IServiceProvider so can use types registered with IServiceCollection, but the filter itself is not resolved from the DI container.
  3. Two variants of invoking an anonymous filter that is the filter with RouteHandlerContext and RouteHandlerFilterDelegate.

In the case that a filter can logically be re-used between multiple endpoints as a singleton but cannot be specified directly when mapping the request delegates (item 1), what is the recommended best practice here with Minimal APIs in ASP.NET Core 7?

An application could do this, but it seems a bit verbose to re-use the same filter instance compared to option 2, except that creates a new instance of the filter for each invocation.

var builder = WebApplication.CreateBuilder(args);

// Register a filter and its dependency as services
builder.Services.AddSingleton<MySingleton>();
builder.Services.AddSingleton<MyFilter>();

var app = builder.Build();

// Filter method that resolves the singleton filter from DI and invokes it
ValueTask<object?> MyFilter(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next)
{
    var filter = context.HttpContext.RequestServices.GetRequiredService<MyFilter>();
    return filter.InvokeAsync(context, next);
}

// Multiple arbitrary endpoints that use the same filter
app.MapGet("/200", () => Results.Ok())
   .AddFilter(MyFilter);

app.MapGet("/500", () => Results.Problem())
   .AddFilter(MyFilter);

app.MapGet("/404", () => Results.NotFound())
   .AddFilter(MyFilter);

app.Run();

// Arbitrary filter implementation that uses a singleton to achieve something
// and can be a singleton in of itself as it has no per-request state so can be re-used
public class MyFilter : IRouteHandlerFilter
{
    public MyFilter(MySingleton singleton)
    {
        Singleton = singleton;
    }

    private MySingleton Singleton { get; }

    public async ValueTask<object?> InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next)
    {
        await Singleton.DoSomething();
        return await next(context);
    }
}

// Arbitrary singleton service that achieves some aim
public class MySingleton
{
    public Task DoSomething() => Task.CompletedTask;
}

Describe the solution you'd like

What's the recommendation on how to author the application for this use-case where minimising the creation of IRouteHandlerFilter instances is desired but it cannot be created up-front when registering the routes?

  • Is there a missing extension method to register a filter that's DI aware, rather than being activated per-request?
  • Is this a use case that will be less verbose to achieve using route groups (Improve minimal api routing with grouping #36007) in later previews?

I thought I'd raise the question about this, as it seems to be a bit of an outlier compared to the rest of Minimal APIs where trying to use a service will get it from DI, otherwise try and parse/bind it or deserialize it from the request body. The AddFilter<T>() method seems to be halfway in between, being able to leverage DI without being in DI itself.

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etc

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions