Minimal API middleware order is confusing and unintuitive
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Is your feature request related to a problem? Please describe the problem.
The way middleware is injected into the request execution pipeline is confusing and unintuitive. This is often confusing for both junior and senior developers as there are subtleties that are only found if you take the time and initiative to carefully read the related documentation, which in itself is confusing as the documentation relating to how all of the different components of the request execution pipeline interact together is spread out across different pages and sections of the ASP.NET documentation.
In the [routing documentation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-9.0), there's even a mention:
> Apps typically don't need to call UseRouting or UseEndpoints. [WebApplicationBuilder](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) configures a middleware pipeline that wraps middleware added in Program.cs with UseRouting and UseEndpoints.
Slightly below it an another mention:
> If the preceding example didn't include a call to UseRouting, the custom middleware would run after the route matching middleware.
In fact, considering that the term _endpoint_ is well understood by most developers as simply _being the destination route that gets served by the server_, most .NET developer don't naturally associate `UseEndpoints` with Minimal APIs due to the aforementioned fact that it is not required to be explicitly called for minimal API endpoints to function.
`UseRouting` is also something that is most likely better understood by the developers and maintainers of the ASP.NET stack itself then by the average .NET developer. The documentation is sort of vague as to what function it actually does and how it's placement influences the rest of the request pipeline as explicitly calling it is optional and it doesn't seem to matter where it's placed as behavior does not appear to change regardless of where it's placed.
Some of the ordering of the middleware is so unintuitive that I still have to refer to [the middleware order diagram](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0#middleware-order) to be able to remember that `UseAuthentication` and `UseAuthorization` have to go after `UseRouting`.
The current new Web API project further reinforces the implicit implementation of the minimal API middleware as there's no mention of either `UseRouting` nor `UseEndpoints`. The template itself doesn't even conform to the current documentation nor expectations as the `MapOpenApi` call is placed before `UseHttpsRedirection`.
```csharp
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5)
.Select(index => new WeatherForecast(DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
)
)
.ToArray();
return forecast;
}
)
.WithName("GetWeatherForecast");
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```
In my experience, most developers do not typically or instinctively refer to documentation during their day to day development, and this is further exacerbated in our new reality which is strongly AI-assisted. Request pipeline ordering is typically something that will lead most developers down a confusing debugging path that will likely end up with a hacky patch instead of simply adding the proper middleware hooks at the right place to ensure predictable and intuitive routing out of the box. I still don't comprehend why `UseRouting` wouldn't go before `UseStaticFiles` when static files are also part of the routing decisions.
With the current defaults, behavior will not reflect intention and a single line can introduce bugs that the developer may not understand.
As a simple example, if we imagine the following fictitious web app:
```csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Minimal API Response");
app.Use((context, next) =>
{
if (context.Request.Path == "/")
{
context.Response.StatusCode = 200;
context.Response.ContentType = "text/plain";
context.Response.WriteAsync("Middleware Response");
return Task.CompletedTask;
}
return next();
}
);
app.Run();
```
The expectation by a developer unfamiliar with the ASP.NET request pipeline is that the minimal API endpoint would trigger first and "Minimal API Response" sent back. The actual behavior is that "Middleware Response" is returned.
If a hook to `next()` is added after the response in the middleware as such:
```csharp
app.Use((context, next) =>
{
if (context.Request.Path == "/")
{
context.Response.StatusCode = 200;
context.Response.ContentType = "text/plain";
context.Response.WriteAsync("Middleware Response");
next(); // Single line added here
return Task.CompletedTask;
}
return next();
}
);
```
Then the response becomes "Middleware ResponseMinimal API Response".
Only once the proper middleware hooks are in place does the behavior represent expectations:
```csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRouting();
app.MapGet("/", () => "Minimal API Response");
app.UseEndpoints(_ => { });
app.Use((context, next) =>
{
if (context.Request.Path == "/")
{
context.Response.StatusCode = 200;
context.Response.ContentType = "text/plain";
context.Response.WriteAsync("Middleware Response");
next();
return Task.CompletedTask;
}
return next();
}
);
app.Run();
```
### Describe the solution you'd like
Proposed solutions and ideas:
- Improve documentation that better explains `UseRouting`, it's purpose and why its ordering matters.
- Improve documentation that better explains `UseStaticFiles` and why it's not affected by `UseRouting`.
- Add a dedicated request pipeline documentation page that gathers all of the different aspects of middleware routing currently spread throughout various pages of documentation.
- Add an overload to `UseEndpoints()` that doesn't require passing an empty function (ie: `app.UseEndpoints(_ => { });`).
- Add additonal comments to the default .NET web templates commenting out various middlewares that are often used in the proper locations where they should be used with links to the related documentation.
#### Example proposal for the Web API template
```csharp
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
// To learn more about middleware orderings and the various middleware types, see the documentation above each method.
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/
app.UseHttpsRedirection();
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing
// app.UseRouting();
// https://learn.microsoft.com/en-us/aspnet/core/security/authentication/
// app.UseAuthentication();
// https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction
// app.UseAuthorization();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5)
.Select(index => new WeatherForecast(DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
)
)
.ToArray();
return forecast;
}
)
.WithName("GetWeatherForecast");
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/middleware
// app.UseEndpoints(_ => { });
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```
### Additional context
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.