17

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(); })
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Miguel Moura
  • 36,732
  • 85
  • 259
  • 481

6 Answers6

27

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();
Soner Gönül
  • 97,193
  • 102
  • 206
  • 364
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
11

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);
...
haiduong87
  • 316
  • 3
  • 14
8

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();
...
Jean-Francois Rondeau
  • 1,008
  • 2
  • 13
  • 15
1

Well, you can have partial Program class:

partial class Program
{
    static void MapProductEndpoints(WebApplication app)
    {
        app.MapGet("/products/{id}", (int id) => Results.Ok());
    }
}

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

or you can have static class or an extension method:

public static class ProductEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/products/{id}", (int id) => Results.Ok());
    }
}

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

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

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

public interface IEndpoints
{
    static abstract void Map(WebApplication app);
}

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

var app = builder.Build();

var assembly = Assembly.GetExecutingAssembly();

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

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

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

mariusz96
  • 66
  • 4
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.

0

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; }
}
Lonli-Lokli
  • 3,357
  • 1
  • 24
  • 40
  • what about RouteGroupBuilder? app.MapGroup how can you do it with ICarterModule? – Vadim Tomashevsky Mar 22 '23 at 08:47
  • It's .NET7 feature, I cannot check it right now but will do when possiblee. – Lonli-Lokli Apr 04 '23 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. – DerHaifisch Aug 15 '23 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 Aug 16 '23 at 07:38
  • @DerHaifisch and this is not service locator pattern - the more appropiate name would be method injection. – mariusz96 Aug 16 '23 at 07:39
  • @mariusz96 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. – DerHaifisch Aug 16 '23 at 14:52
  • @mariusz96 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. – DerHaifisch Aug 16 '23 at 14:54