23

In .NET 6 it is possible to create minimal APIs:

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

app.MapGet("/products/{id}", (int id) => { return Results.Ok(); })
app.MapGet("/users/{id}", (int id) => { return Results.Ok(); })

app.Run();

What would be an approach to group endpoints in multiple files instead of having all in Program file?

ProductEndpoints.cs:

app.MapGet("/products/{id}", (int id) => { return Results.Ok(); })

UserEndpoints.cs

app.MapGet("/users/{id}", (int id) => { return Results.Ok(); })

7 Answers 7

38

Only one file with top-level statement is allowed per project. But nobody forbids moving endpoints to some static method of another class:

public static class ProductEndpointsExt
{
    public static void MapProductEndpoints(this WebApplication app)
    {
        app.MapGet("/products/{id}", (int id) => { return Results.Ok(); });
    }
}

And in the Program file:

app.MapProductEndpoints();
0
13

We can use partial Program.cs files too

Example: "Program.Users.cs"

partial class Program
{
    /// <summary>
    /// Map all users routes
    /// </summary>
    /// <param name="app"></param>
    private static void AddUsers(WebApplication app)
    {
        app.MapGet("/users", () => "All users");
        app.MapGet("/user/{id?}", (int? id) => $"A users {id}");
        ///post, patch, delete...
    }
}

And in "Program.cs"

...
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
//add...
AddUsers(app);
...
12

What I did is creating a interface IEndPoint that each class that need to define endpoints must implement, and an extension method to find all implementations to call the interface mapping method. You just have to call that extension method in your Program.cs or Startup to register all the endpoints.

// IEndpoint.cs
public interface IEndPoint
{
    void MapEndpoint(WebApplication app);
}
// FeatureA.cs
public class FeatureA: IEndPoint
{
    public void MapEndpoint(WebApplication app)
    {
        app.MapGet("api/FeatureA/{id}", async (int id) => $"Fetching {id} data");
    }
}
// WebApplicationExtension.cs
public static class WebApplicationExtensions
{
    public static void MapEndpoint(this WebApplication app)
    {
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
            
        var classes = assemblies.Distinct().SelectMany(x => x.GetTypes())
            .Where(x => typeof(IEndPoint).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract);

        foreach (var classe in classes)
        {
            var instance = Activator.CreateInstance(classe) as IEndPoint;
            instance?.MapEndpoint(app);
        }
    }
}
// Program.cs
...
app.MapEndpoint();
...
2
  • I really like this approach, thank you! Commented Feb 2, 2023 at 23:38
  • 1
    Part of the reason Minimal APIs were developed was to make a more functional approach to setting up endpoints. Before, we only had controller based APIs, where you would call app.MapControllers(); which would magically (using reflection) find anything which inherits from ControllerBase. What you've done here is reinvented controller based APIs but using the Minimal API methods! Read up on the differences between the two approaches here: learn.microsoft.com/en-us/aspnet/core/fundamentals/… (rather than inventing your own mix)
    – harvzor
    Commented Jan 19 at 16:21
5

Update

With .Net7. You now have the option of MapGroup.

Example:

 public static class GroupEndpointsExt
 {
     public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
     {
         group.MapGet("/", GetAllTodos);
         group.MapGet("/{id}", GetTodo);
         group.MapPost("/", CreateTodo);
         group.MapPut("/{id}", UpdateTodo);
         group.MapDelete("/{id}", DeleteTodo);

         return group;
     }
 }

Your Program.cs.

var root = app.MapGroup("minimalapi");
root.MapTodosApi();
1
  • If you are trying to add the RouteGroupBuilder to a library project, you will need to add a reference to AspNetCore.App. See this answer on how to do it. stackoverflow.com/a/77265555/1193670
    – epak96
    Commented Jul 2 at 14:34
3

Well, you can have partial Program class:

partial class Program
{
    static IEndpointRouteBuilder MapProductEndpoints(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/products/{id}", (int id) => Results.Ok());
        return endpoints;
    }
}

var app = builder.Build();
MapProductEndpoints(app);

or you can have static class or an extension method:

public static class ProductEndpoints
{
    public static IEndpointRouteBuilder Map(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/products/{id}", (int id) => Results.Ok());
        return endpoints;
    }
}

var app = builder.Build();
ProductEndpoints.Map(app);
public static class EndpointRouteBuilderProductEndpointsExtensions
{
    public static IEndpointRouteBuilder MapProductEndpoints(this IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/products/{id}", (int id) => Results.Ok());
        return endpoints;
    }
}

var app = builder.Build();
app.MapProductEndpoints();

or you can wrap it in an interface and do assembly scanning or a source generator:

public interface IEndpoints
{
    static IEndpointRouteBuilder Map(IEndpointRouteBuilder endpoints);
}

public class ProductEndpoints : IEndpoints
{
    public static IEndpointRouteBuilder Map(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/products/{id}", (int id) => Results.Ok());
        return endpoints;
    }
}

var app = builder.Build();

var assembly = Assembly.GetExecutingAssembly();

var endpointsCollection = assembly
    .GetTypes()
    .Where(type => !type.IsInterface && type.GetInterfaces().Contains(typeof(IEndpoints)));

foreach (var endpoints in endpointsCollection)
{
    var map = endpoints.GetMethod(nameof(IEndpoints.Map));
    map.Invoke(null, new[] { app });
}

https://dev.to/joaofbantunes/mapping-aspnet-core-minimal-api-endpoints-with-c-source-generators-3faj.

You can also try to do endpoint per file though that's trickier to enforce😅.

1

Another option is to use Carter project

  1. Add carter project to Nuget dotnet add package carter

  2. Modify Program.cs to use carter

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCarter();

var app = builder.Build();

app.MapCarter();
app.Run();

Notice that .AddControllers() can be removed

  1. Add Carter Module, it will be later auto-discovered
using Carter;
using MapEndpoints;

public class WeatherModule : ICarterModule
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/GetWeatherForecast", (ILoggerFactory loggerFactory) => Enumerable.Range(1, 5).Select(index =>
                new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = Random.Shared.Next(-20, 55),
                    Summary = Summaries[Random.Shared.Next(Summaries.Length)]
                })
            .ToArray());
    }
}

public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string? Summary { get; set; }
}
9
  • what about RouteGroupBuilder? app.MapGroup how can you do it with ICarterModule? Commented Mar 22, 2023 at 8:47
  • It's .NET7 feature, I cannot check it right now but will do when possiblee. Commented Apr 4, 2023 at 13:34
  • I don’t know why this was downvoted. It answers the question. All of the solutions I have seen use static methods that don’t allow DI from the constructor or properties. This means you have to use service location. Carter allows you to group your endpoints like MapGroup and allows you to use DI, but without using static classes and methods. Commented Aug 15, 2023 at 12:00
  • @DerHaifisch it doesn't though - it registers modules as singletons and as such you cannot ie inject new instance of dbcontext per HTTP request into the module's constructor.
    – mariusz96
    Commented Aug 16, 2023 at 7:38
  • @DerHaifisch and this is not service locator pattern - the more appropiate name would be method injection.
    – mariusz96
    Commented Aug 16, 2023 at 7:39
0

I think the best way is to use Controller based web service. Although, you can this approach like this:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapWeatherForecastRoutes();

app.Run();

internal static class WeatherForecastController
{
    internal static void MapWeatherForecastRoutes(this WebApplication app)
    {
        app.MapGet("/weatherforecast", () =>
            {
                var summaries = new[]
                {
                        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
                };

                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")
            .WithOpenApi();
    }
}

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

The only thing we need to consider is how to make the best use of extension methods. It is enough to implement each group of web services in a static class and add them to the program using Extension methods.

Not the answer you're looking for? Browse other questions tagged or ask your own question.