dotnet / dotnet/aspnetcore

[Route Groups] Investigate callback based API

Open
#41,432 2 comments 2 reactions 0 assignees View on GitHub
area-minimal
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.

A callback-based version of `MapGroup` was considered as an [alternative design in the [original route grouping issue](https://github.com/dotnet/aspnetcore/issues/36007#issuecomment-1087774396). In this design, the `IEndpointRouteBuilder` would be passed to a callback rather than returned from `MapGroup`.

### Describe the solution you'd like

If we use callbacks instead of the return type of `MapGroup` to define endpoints, it naturally leads to a nested structure in the cod that matches the nested structure of a group. For example:

```csharp
var builder = WebApplication.CreateBuilder(args);
// ...
var app = builder.Build();

app.MapGroup("/todos", group =>
{
group.MapGet("/", (int id, TodoDb db) => db.ToListAsync());
group.MapGet("/{id}", (int id, TodoDb db) => db.GetAsync(id));
group.MapPost("/", (Todo todo, TodoDb db) => db.AddAsync(todo));

// string org cannot be an argument to the configureGroup callback because that would require MapGet and other
// IEndpointRouteBuilder extension methods to be repeatedly called fore every request.
group.MapGroup("/{org}", nestedGroup =>
{
nestedGroup.MapGet("/", (string org, TodoDb db) => db.Filter(todo => todo.Org == org).ToListAsync());
// ...
}).RequireAuthorization();
// ....
}).RequireCors("AllowAll");
// ...
```

So instead of what we have today:

```csharp
var builder = WebApplication.CreateBuilder(args);
// ...
var app = builder.Build();

var group = app.MapGroup("/todos");
group.MapGet("/", (int id, TodoDb db) => db.ToListAsync());
group.MapGet("/{id}", (int id, TodoDb db) => db.GetAsync(id));
group.MapPost("/", (Todo todo, TodoDb db) => db.AddAsync(todo));
group.RequireCors("AllowAll");

var nestedGroup = group.MapGroup("/{org}");
nestedGroup.MapGet("/", (string org, TodoDb db) => db.Filter(todo => todo.Org == org).ToListAsync());
nestedGroup.RequireAuthorization();
// ...
```

You an already manually introduce scopes to get a similar kind of structure with the existing API as demonstrated by https://twitter.com/davidfowl/status/1519480212060139521, but I doubt many people will structure their code like this in practice unless we use callbacks:

```csharp
var builder = WebApplication.CreateBuilder(args);
// ...
var app = builder.Build();

var group = app.MapGroup("/todos");
{
group.MapGet("/", (int id, TodoDb db) => db.ToListAsync());
group.MapGet("/{id}", (int id, TodoDb db) => db.GetAsync(id));
group.MapPost("/", (Todo todo, TodoDb db) => db.AddAsync(todo));
group.RequireCors("AllowAll");

var nestedGroup = group.MapGroup("/{org}");
{
nestedGroup.MapGet("/", (string org, TodoDb db) => db.Filter(todo => todo.Org == org).ToListAsync());
nestedGroup.RequireAuthorization();
}
}
// ...
```

### Additional context

The current API and a callback-based API are not mutually exclusive. These are different overloads, so both could be supported. I don't like this however, because I think having too many ways to do things does more harm by creating confusion than it helps by providing more flexibility.

If we do decide to support both, we'd need to decide would `MapGroup` still return a `GroupRouteBuilder` that you can add endpoints on? And would the callback parameter also be a `GroupRouteBuilder` meaning you could add both routes and conventions both inside and outside the callback? In our docs, would we normally add endpoints inside the callback and conventions outside the callback like in the sample above?

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.