Sviluppare API Web in .NET Core ha tradizionalmente significato utilizzare il framework MVC con i controller. Tuttavia, con l’introduzione di .NET 6, Microsoft ha presentato un nuovo approccio: le Minimal API. Questo articolo esplora le differenze tra i due approcci, analizzando vantaggi e svantaggi di entrambi per aiutarti a scegliere l’opzione più adatta al tuo progetto.
Se sei già uno sviluppatore .NET con esperienza nei controller tradizionali ma non hai ancora esplorato le Minimal API, questo articolo è pensato appositamente per te.
Cosa sono le Minimal API?
Le Minimal API, introdotte con .NET 6, rappresentano un approccio semplificato alla creazione di API Web in ASP.NET Core. Come suggerisce il nome, sono progettate per ridurre al minimo il codice boilerplate e le dipendenze necessarie per creare un’API funzionante.
Ecco un semplice esempio di una Minimal API che risponde a una richiesta GET:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello, World!");
app.Run();
Con queste poche righe di codice, abbiamo creato un’API completamente funzionante che risponde a richieste GET sull’endpoint /hello restituendo la stringa “Hello, World!”.
Controller vs Minimal API: le differenze fondamentali
Struttura del codice
Controller
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
Minimal API
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
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55)
Summary = summaries[rng.Next(summaries.Length)]
})
.ToArray();
return forecast;
});
Configurazione e startup
Controller:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
// Altre configurazioni...
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Minimal API
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Aggiungi servizi
var app = builder.Build();
// Configura il middleware
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
// Definisci le rotte
app.MapGet("/products", () => /* ... */);
app.MapPost("/products", (Product product) => /* ... */);
app.Run();
I vantaggi delle Minimal API
Meno codice boilerplate
Le Minimal API riducono significativamente la quantità di codice necessario per creare un’API, le si possono inserire addirittura nel Program.cs (anche se questa non è una best-practice, ma lo vediamo più avanti in questo articolo). Quando creiamo un nuovo progetto .NET API da Visual Studio, le Minimal API sono l’opzione di default.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Il codice precedente rappresenta un’applicazione Web API completa – difficile essere più minimalisti di così!
Startup più semplice e veloce
Un’applicazione Minimal API può essere avviata più rapidamente perché carica meno dipendenze rispetto a un’applicazione completa basata su MVC (anche dal punto di vista della RAM il consumo è minore).
Performance
Le Minimal API sono state progettate tenendo a mente le prestazioni. Rimuovendo le parti non necessarie del framework MVC, si ottiene un’applicazione più leggera con un minor sovraccarico. Questo aspetto è molto “sentito” da Microsoft, che sta puntando molto su questo aspetto. Con .NET 9 le allocazioni di memoria per request nelle Minimal API sono state ridotte fino al 93%. Ogni nuova versione di .NET è sempre più veloce della precedente, grazie anche alle migliorie a JIT o la compilazione AOT.
Native AOT
Le Minimal API hanno supporto nativo per la compilazione AOT (Ahead-of-Time), a differenza dei controller classici che non lo supportano. Questo le rende la scelta obbligata in scenari dove il cold start e il consumo di memoria sono critici, come nelle funzioni serverless o nei container con risorse limitate.
Validazione
Con .NET 10 è stata aggiunta la validazione integrata (nelle versioni precedenti la validazione andava eseguita a mano, implementando controlli specifici nei metodi o usando librerie di terze parti come FluentValidation).
// Abilitazione della validazione per tutti gli endpoint
builder.Services.AddValidation();
public record Product(
[Required] string Name,
[Range(1, 1000)] int Quantity,
[EmailAddress] string? ContactEmail);
app.MapPost("/products", (Product product) => TypedResults.Created($"/products/{product.Name}", product));
// Disabilitazione della validazione su uno specifico endpoint
app.MapPost("/products-import", (ProductBatch batch) => TypedResults.Ok())
.DisableValidation();
Programmazione funzionale
Le Minimal API incoraggiano uno stile di programmazione più funzionale, utilizzando lambda e funzioni anziché classi e metodi, il che può portare a un codice più conciso per API semplici.
app.MapGet("/user/{id}", (int id) => GetUser(id));
app.MapPost("/user", (User user) => CreateUser(user));
app.MapPut("/user/{id}", (int id, User user) => UpdateUser(id, user));
app.MapDelete("/user/{id}", (int id) => DeleteUser(id));
Limiti delle Minimal API
Nonostante i vantaggi, le Minimal API presentano alcune limitazioni che è importante conoscere:
Meno struttura per progetti complessi
Per progetti di grandi dimensioni, la mancanza di una struttura formale come quella fornita dai controller può portare a un codice disorganizzato o difficile da mantenere. Il mio consiglio è di non inserire gli endpoint direttamente nel Program.cs ma di creare sempre delle classi di estensione dedicate, dove suddividere ed organizzare gli endpoint.
Nessun supporto agli Action Filter nativi
Le Minimal API non supportano gli Action Filter dei controller MVC. In sostituzione si utilizzano gli IEndpointFilter, che offrono funzionalità simili ma con una sintassi diversa. È un aspetto da tenere in considerazione se stai migrando un progetto esistente che fa uso intenso di filtri.
Best practice quando usiamo le Minimal API
Se decidi di utilizzare le Minimal API, ecco alcune best practice da seguire:
1. Organizza il codice in file separati
Le Minimal API possono essere scritte direttamente nel Program.cs, ma non è una buona pratica scrivere tutto qui dentro. Un progetto ben organizzato è un requisito indispensabile, le Minimal API vanno quindi organizzate in classi dedicate:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapProductEndpoints();
app.Run();
public static class ProductEndpoints
{
public static WebApplication MapProductEndpoints(this WebApplication app)
{
app.MapGet("/products", GetAllProducts);
app.MapGet("/products/{id}", GetProductById);
app.MapPost("/products", CreateProduct);
return app;
}
private static IResult GetAllProducts() => /* ... */;
private static IResult GetProductById(int id) => /* ... */;
private static IResult CreateProduct(Product product) => /* ... */;
}
2. Utilizza i gruppi di endpoint
Dalla versione 7 di .NET puoi organizzare le tue API in gruppi. Con questo approccio si possono aggiungere caratteristiche ad hoc in funzione del gruppo (es. RequireAuthorization()) anziché ripeterlo per ogni endpoint mappato.
var productsGroup = app.MapGroup("/products")
.WithTags("Products") // Tag OpenAPI
.RequireAuthorization(); // Applica autorizzazione a tutte le rotte nel gruppo
productsGroup.MapGet("/", GetAllProducts);
productsGroup.MapGet("/{id}", GetProductById);
productsGroup.MapPost("/", CreateProduct);
productsGroup.MapPut("/{id}", UpdateProduct);
productsGroup.MapDelete("/{id}", DeleteProduct);
3. Validazione del modello
Esistono diverse opzioni per la validazione dei modelli nelle Minimal API:
Opzione 1: Validazione out-of-the-box (.NET 10+)
Dal .NET 10 è possibile abilitare la validazione nativa con una sola riga, usando gli stessi attributi di data annotation già noti dai controller classici:
builder.Services.AddValidation();
app.MapPost("/products", (Product product) => TypedResults.Created($"/products/{product.Name}", product));
Opzione 2: Usando FluentValidation (installare il pacchetto NuGet)
app.MapPost("/products", async (IValidator<Product> validator, Product product) =>
{
var validationResult = await validator.ValidateAsync(product);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(validationResult.ToDictionary());
}
// Logica di creazione
// [...]
});
4. Dependency Injection
L’iniezione delle dipendenze funziona già out-of-the-box con le Minimal API, basterà richiamare l’interfaccia come parametro, analogamente a come già facciamo nei costruttori delle classi.
app.MapGet("/products",
async (IProductRepository repository) =>
{
return await repository.GetAllProductsAsync();
});
app.MapGet("/products/{id}",
async (int id, IProductRepository repository) =>
{
var product = await repository.GetProductByIdAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
});
Quando usare le Minimal API?
Abbiamo visto i pro e contro, le best practice e come usarle.. ma quando ci conviene scegliere questo approccio a discapito dei classici controller?
- Microservizi: Quando hai bisogno di servizi leggeri con un numero limitato di endpoint.
- API semplici: Per API con pochi endpoint e logica semplice. In caso di eccessiva complessità preferire i classici controller oppure centralizzare in servizi dedicati (da iniettare tramite dependency injection).
- Prototipazione rapida: Quando hai bisogno di creare rapidamente un’API funzionante.
- Native AOT e serverless: Le Minimal API sono l’unica scelta quando si compila con AOT o si lavora in ambienti serverless dove il cold start è critico. I controller MVC non supportano Native AOT.
- Progetti didattici: Ottimo per imparare e dimostrare concetti di API REST senza il sovraccarico delle convenzioni MVC. In caso di progetti Enterprise le Minimal API vanno sempre considerate in funzione della complessità e dell’organizzazione del progetto.
Esempio di utilizzo
Vediamo un confronto pratico implementando la stessa API in entrambi gli stili, ipotizzando di dover creare una serie di endpoint per gestire un catalogo prodotti.
Approccio con controller:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
/// <summary>
/// Get products
/// </summary>
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
var products = await _repository.GetAllAsync();
return Ok(products);
}
/// <summary>
/// Get product
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
{
return NotFound();
}
return product;
}
/// <summary>
/// Create product
/// </summary>
[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _repository.AddAsync(product);
return CreatedAtAction(
nameof(GetProduct),
new { id = product.Id },
product);
}
/// <summary>
/// Update product
/// </summary>
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, Product product)
{
if (id != product.Id)
{
return BadRequest();
}
var exists = await _repository.ExistsAsync(id);
if (!exists)
{
return NotFound();
}
await _repository.UpdateAsync(product);
return NoContent();
}
/// <summary>
/// Delete product
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var exists = await _repository.ExistsAsync(id);
if (!exists)
{
return NotFound();
}
await _repository.DeleteAsync(id);
return NoContent();
}
}
Approccio con Minimal API:
Per questione di semplificazione è tutto dentro il Program.cs, gli endpoint andrebbero comunque estratti ed inseriti in una classe dedicata, come visto prima in questo tutorial.
var builder = WebApplication.CreateBuilder(args);
// Supporto OpenAPI nativo (disponibile dal .NET 9, non richiede Swashbuckle)
builder.Services.AddOpenApi();
// DI registration
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
// Espone il documento OpenAPI su /openapi/v1.json
app.MapOpenApi();
// Opzionale: aggiungi una UI come Scalar (pacchetto Scalar.AspNetCore)
// app.MapScalarApiReference();
}
app.UseHttpsRedirection();
// Mappa i tuoi endpoint lasciando il Program.cs pulito
app.MapProductEndpoints();
app.Run();
Sopra il Program.cs, sotto gli endpoint centralizzati in una classe dedicata:
public static class ProductEndpoints
{
public static WebApplication MapProductEndpoints(this WebApplication app) {
var group = app.MapGroup("/api/products")
.WithTags("Products");
group.MapGet("/", GetAllProductsAsync)
.WithName("GetProducts");
group.MapGet("/{id}", GetProductByIdAsync)
.WithName("GetProduct");
group.MapPost("/", CreateProductAsync)
.WithName("CreateProduct");
group.MapPut("/{id}", UpdateProductAsync)
.WithName("UpdateProduct");
group.MapDelete("/{id}", DeleteProductAsync)
.WithName("DeleteProduct");
return app;
}
// ****************************
// GET products
// ****************************
private static async Task<IResult> GetAllProductsAsync(IProductRepository repository)
{
return Results.Ok(await repository.GetAllAsync());
}
// ****************************
// GET product by ID
// ****************************
private static async Task<IResult> GetProductByIdAsync(int id, IProductRepository repository)
{
var product = await repository.GetByIdAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
}
// ****************************
// Create product
// ****************************
private static async Task<IResult> CreateProductAsync(Product product, IProductRepository repository)
{
await repository.AddAsync(product);
return Results.CreatedAtRoute("GetProduct", new { id = product.Id }, product);
}
// ****************************
// Update product
// ****************************
private static async Task<IResult> UpdateProductAsync(int id, Product product, IProductRepository repository)
{
if (id != product.Id) {
return Results.BadRequest();
}
var exists = await repository.ExistsAsync(id);
if (!exists) {
return Results.NotFound();
}
await repository.UpdateAsync(product);
return Results.NoContent();
}
// ****************************
// Delete product
// ****************************
private static async Task<IResult> DeleteProductAsync(int id, IProductRepository repository)
{
var exists = await repository.ExistsAsync(id);
if (!exists)
{
return Results.NotFound();
}
await repository.DeleteAsync(id);
return Results.NoContent();
}
}
Conclusioni
Le Minimal API rappresentano un’evoluzione interessante nello sviluppo Web con .NET, offrendo un approccio più snello e diretto rispetto all’utilizzo dei classici controller. Tuttavia, non sono una soluzione universale, la scelta tra Minimal API e controller tradizionali dipende dalle specifiche esigenze del tuo progetto.
Ormai sono diverse versioni di .NET che le Minimal API sono state introdotte, ed è già lo standard nel momento in cui creiamo un nuovo progetto Web API. Il mio consiglio è di iniziare a sperimentare con le Minimal API in progetti di piccole dimensioni: questo ti permetterà di valutare se e come questo nuovo approccio si adatta al tuo stile di sviluppo e alle esigenze del tuo team, ma ti permettono anche di iniziare a familiarizzare con un qualcosa che sarà sempre più la normalità in progetti di questo tipo.