Minimal API in .NET 6 using multiple files
Asked Answered
F

7

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(); })
Franck answered 27/11, 2021 at 23:2 Comment(0)
B
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();
Benbena answered 27/11, 2021 at 23:16 Comment(0)
C
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);
...
Chaucerian answered 16/1, 2022 at 18:13 Comment(0)
U
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();
...
Uropygium answered 28/10, 2022 at 16:4 Comment(2)
I really like this approach, thank you!Jenellejenesia
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)Stomodaeum
C
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();
Connally answered 29/9, 2023 at 14:47 Comment(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. https://mcmap.net/q/584227/-failure-to-install-microsoft-aspnetcore-apprefLangham
G
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😅.

Grallatorial answered 10/8, 2023 at 8:33 Comment(0)
O
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; }
}
Otti answered 14/11, 2022 at 15:33 Comment(9)
what about RouteGroupBuilder? app.MapGroup how can you do it with ICarterModule?Deduction
It's .NET7 feature, I cannot check it right now but will do when possiblee.Otti
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.Frei
@Frei 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.Grallatorial
@Frei and this is not service locator pattern - the more appropiate name would be method injection.Grallatorial
@Grallatorial I haven't run into issues using Carter. I guess because I'm injecting Mediator into the constructor and handling the business logic in a command/query handler elsewhere where the necessary dependencies are injected. Regardless, my understanding is that you can still pass dependencies in via the route functions. It would be interesting to fiddle with the MapGroup solution and the method suggested by the accepted solution.Frei
@Grallatorial I forgot about the fact that you could pass in dependencies via the route functions and so because of that lapse was thinking that the only way to get the dependencies would be via service location.Frei
@Grallatorial I just wanted to leave this here as a thank you. I used your answer to this question in a project I am working on and was able to remove Carter. Your answer helped me and I learned something new. Thanks again!Frei
@Frei very happy you feel this way :) I'm personally not against Carter, though best practice usage could be somehow counterintuitive: github.com/CarterCommunity/Carter/issues/279 github.com/CarterCommunity/Carter/issues/288.Grallatorial
A
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.

Accumulative answered 5/10, 2022 at 7:39 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.