Le Minimal APIs in ASP.NET rappresentano un approccio piΓΉ leggero e semplificato per creare API REST rispetto al tradizionale ASP.NET Web API. Introdotte con ASP.NET Core 6, le Minimal APIs eliminano molte delle configurazioni e del codice boilerplate richiesto dalle API tradizionali, offrendo un modo piΓΉ conciso e diretto per definire gli endpoint. Microsoft non ha intenzione di deprecare la modalitΓ tradizionale con in controller in quanto esistono degli scenari complessi dove puΓ² ancora funzionare bene ma lβidea Γ¨ poter fare il 90% delle API possibili in modalitΓ minimal.
Differenze tra Minimal API e API tradizionali
| Caratteristica | Minimal API | API tradizionali |
|---|---|---|
| Struttura del codice | PiΓΉ semplice e concisa | PiΓΉ strutturata e verbosa |
| Controller | Non richiede controller | Usa controller e action |
| Configurazione | Minima, senza bisogno di attributi [Route] o [HttpGet] | Necessita di configurazioni piΓΉ dettagliate |
| Performance | PiΓΉ veloce, meno overhead | Leggermente piΓΉ pesante per via del framework MVC |
| Middleware | Definito direttamente in Program.cs | Basato su unβarchitettura con piΓΉ classi e dipendenze |
Esempio
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello, Minimal API!");
app.MapGet("/users", () =>
{
return new List<string> { "Alice", "Bob", "Charlie" };
});
app.Run();- Non usa controller nΓ© action.
- Gli endpoint sono definiti direttamente nella pipeline dellβapplicazione.
- Utilizza
app.MapGet()per definire i metodi HTTP senza bisogno di[HttpGet]. - Il codice Γ¨ piΓΉ compatto e leggibile.
DI Service Registration
La registrazione dei servizi per la DI avviene prima della creazione dellβapp con builder.Build(), cioΓ¨ tra CreateBuilder e Build().
var builder = WebApplication.CreateBuilder(args);
// π Service registration qui
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<GuidGenerator>();
builder.Services.AddSingleton<PeopleService>();
// π Fine service registration
var app = builder.Build();Middleware
Registration
La registrazione dei middleware avviene dopo builder.Build(), quindi sul risultato di var app = builder.Build();.
Γ il punto in cui si configura il comportamento della pipeline HTTP.
var builder = WebApplication.CreateBuilder(args);
// Service registration
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// π Middleware registration qui
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseAuthorization();
// π Fine middleware registration
// Routing
app.MapControllers();Parametri
Passare parametri
I parametri possono essere passati ad un metodo in 5 modi diversi, vediamoli tutti.
Route
I route parameters permettono di estrarre valori direttamente dallβURL. Ecco alcune possibilitΓ :
- Tipizzazione:
"{age:int}"β il parametroagedeve essere un intero.
β Viene automaticamente convertito inint agenel metodo handler. - Espressione Regolare:
"{carId:regex(^[a-z0-9]+$)}"βcarIddeve rispettare il pattern regex (solo lettere minuscole e numeri).
β Se il valore non rispetta la regex, la route non viene attivata. - Lunghezza Fissa:
"{isbn:length(13)}"βisbndeve avere esattamente 13 caratteri.
Query string
I parametri possono essere passati tramite la query string dellβURL (es. ?query=123).
Per leggerli, si usa lβattributo [FromQuery].
[FromQuery(Name = "query")] int queryParamIl nome puΓ² essere specificato esplicitamente con Name = "...", utile se diverso dal nome della variabile.
DI Container
Γ possibile ricevere oggetti o servizi direttamente dal Dependency Injection container, semplicemente dichiarandoli nei parametri del metodo.
ASP.NET li inietta automaticamente se sono registrati nel container.
GuidGenerator guidGeneratorQui GuidGenerator Γ¨ un servizio risolto tramite DI, senza bisogno di alcun attributo.
Header
Γ possibile leggere valori direttamente dagli header HTTP tramite [FromHeader].
Utile per accedere a metadati della richiesta.
[FromHeader(Name = "Accept-Encoding")] string encodingIl valore viene estratto dallβheader Accept-Encoding.
Body (JSON)
Γ possibile ricevere oggetti complessi inviati nel corpo della richiesta (body), ad esempio in formato JSON.
Si usa lβattributo [FromBody], anche se Γ¨ implicito per i tipi complessi.
app.MapPost("/create", ([FromBody] Product product) =>
{
return $"Received product: {product.Name}";
});Se il tipo Product Γ¨ una classe con proprietΓ come Name, Price, ecc., queste verranno automaticamente deserializzate dal JSON nel body della richiesta.
Parametri speciali
ASP.NET Minimal API permette anche di accedere a oggetti speciali del framework, che forniscono informazioni avanzate sulla richiesta o sul contesto di esecuzione. Questi parametri vengono riconosciuti automaticamente dal runtime, senza bisogno di attributi.
HttpContext
Fornisce accesso completo a tutti gli elementi della richiesta e della risposta HTTP.
app.MapGet("httpcontext", (HttpContext context) =>
{
return Results.Ok($"Request path: {context.Request.Path}");
});HttpRequest / HttpResponse
Γ possibile accedere direttamente solo alla richiesta o alla risposta.
app.MapGet("http", (HttpRequest request, HttpResponse response) =>
{
var queries = request.QueryString.Value;
return response.WriteAsync($"Query string: {queries}");
});ClaimsPrincipal (utente corrente)
Permette di accedere allβutente autenticato e ai suoi claim.
app.MapGet("claims", (ClaimsPrincipal user) =>
{
var name = user.Identity?.Name;
return Results.Ok($"User: {name}");
});CancellationToken
Consente di gestire lβannullamento della richiesta, ad esempio se il client chiude la connessione.
app.MapGet("cancel", (CancellationToken token) =>
{
// PuΓ² essere usato per interrompere operazioni lunghe
return Results.Ok("CancellationToken received");
});Custom parameter binding
Γ un meccanismo che consente di personalizzare il modo in cui Minimal API converte i dati della richiesta (route, query, body, ecc.) in oggetti da passare come parametri allβhandler.
Per esempio voglio passare un MapPoint in un metodo POST
public class MapPoint
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public static bool TryParse(string? value, out MapPoint? point) { ... }
public static async ValueTask<MapPoint?> BindAsync(HttpContext context, ParameterInfo parameterInfo) { ... }
}
//...
app.MapPost("/map", (MapPoint point) =>
{
return Results.Ok($"Lat: {point.Latitude}, Lng: {point.Longitude}");
});ASP.NET cerca automaticamente uno dei seguenti metodi statici:
public static bool TryParse(string, out T)public static ValueTask<T?> BindAsync(HttpContext, ParameterInfo)
1. TryParse β binding da route o query string
public static bool TryParse(string? value, out MapPoint? point)- Permette il binding da route o query quando il parametro arriva come stringa (
lat,lng). - Il framework usa
TryParse()per convertire il valore in un oggettoMapPoint. - Esempio URL:
/location/45.0,9.0 - ASP.NET userΓ
MapPoint.TryParse("45.0,9.0", out var point).
2. BindAsync β binding personalizzato da body o contesto
public static async ValueTask<MapPoint?> BindAsync(HttpContext context, ParameterInfo parameterInfo)- Viene usato se il framework non trova un
TryParseadatto oppure se il tipo Γ¨ passato tramite body, header, o da altre fonti personalizzate. - In questo esempio, legge il body della richiesta (es.
"45.0,9.0") e lo trasforma in unMapPoint. - Esempio body HTTP:
45.0,9.0 - ASP.NET chiamerΓ
MapPoint.BindAsync(...).
Swagger
Swagger permette di avere una visualizzazione carina delle API nel browser. Eβ possibile aggiungere delle extension alle API in modo da migliorare lβoutput di Swagger, vediamo quali.
Accepts<T>()
.Accepts<Book>("application/json")- Specifica il tipo di contenuto accettato dal client (nel body della richiesta).
- Serve principalmente a generare documentazione Swagger/OpenAPI corretta.
- In questo caso, Swagger capisce che il client deve inviare un oggetto
Booknel formatoapplication/json.
Produces<T>(statusCode)
.Produces<Book>(201)
.Produces<IEnumerable<ValidationFailure>>(400)- Indica i possibili tipi di risposta e i relativi codici HTTP restituiti dallβendpoint.
- Anche queste estensioni sono usate solo per Swagger/OpenAPI, non influenzano lβesecuzione dellβAPI.
- Aiutano a documentare in modo esplicito:
- Che tipo di oggetto verrΓ restituito
- Con quale codice di stato
Riassunto
| Estensione | Scopo | Esempio |
|---|---|---|
.Accepts<T>(mediaType) | Specifica il tipo accettato nel body della richiesta e il media type (es. application/json) | .Accepts<Book>("application/json") |
.Produces<T>(statusCode) | Documenta il tipo restituito nella risposta insieme al codice HTTP | .Produces<Book>(201) |
.Produces(statusCode) | Documenta solo il codice di stato restituito, senza specificare il tipo | .Produces(204) |
.ProducesProblem(statusCode) | Documenta che lβendpoint puΓ² restituire una ProblemDetails (errori standard RFC 7807) | .ProducesProblem(400) |
.WithName(name) | Assegna un nome allβendpoint per routing e Swagger | .WithName("CreateBook") |
.WithTags(tags) | Raggruppa lβendpoint sotto uno o piΓΉ tag nella documentazione Swagger | .WithTags("Books") |
.ExcludeFromDescription() | Esclude lβendpoint dalla documentazione Swagger/OpenAPI | .ExcludeFromDescription() |
CORS
CORS (Cross-Origin Resource Sharing) Γ¨ un meccanismo di sicurezza dei browser che regola le richieste HTTP tra origini diverse (es. un frontend su http://localhost:3000 che chiama unβAPI su http://localhost:5000).
Di default, i browser bloccano queste richieste cross-origin, a meno che il server non consenta esplicitamente lβaccesso.
Come abilitare CORS in Minimal API
1. Registrazione della policy CORS nei servizi
Nel Program.cs, nella parte di service registration:
builder.Services.AddCors(options =>
{
options.AddPolicy("AnyOrigin", policy =>
policy.AllowAnyOrigin() // Consente tutte le origini
.AllowAnyHeader()
.AllowAnyMethod());
});2. Abilitare CORS nella pipeline middleware
Nel blocco middleware, dopo app.Build():
app.UseCors(); // usa la policy di defaultOppure per usare una policy specifica:
app.UseCors("AnyOrigin");3. Associare CORS ad un singolo endpoint (opzionale)
Se vuoi applicare CORS solo a certe API, puoi farlo direttamente nellβendpoint:
app.MapGet("status", [EnableCors("AnyOrigin")] () =>
{
return Results.Ok("Status ok");
});Oppure:
app.MapPost("books", (...) => { ... })
.RequireCors("AnyOrigin");Struttura file
In unβapp Minimal API, Γ¨ facile che il file Program.cs diventi troppo lungo e disordinato.
Per risolvere questo problema, puoi organizzare gli endpoint in classi dedicate, grazie a un approccio modulare basato su:
- unβinterfaccia comune (
IEndpoints) - una classe per ogni gruppo di endpoint (es.
LibraryEndpoints) - due metodi di estensione:
AddEndpointseUseEndpoints
1. Interfaccia IEndpoints
Definisce un contratto standard per ogni classe di endpoint:
public interface IEndpoints
{
static abstract void DefineEndpoints(IEndpointRouteBuilder app);
static abstract void AddServices(IServiceCollection services, IConfiguration configuration);
}DefineEndpointsβ mappa le API.AddServicesβ registra i servizi usati dagli endpoint (es.IBookService).
2. Implementazione in LibraryEndpoints
Qui vengono definiti gli endpoint e i servizi specifici della libreria:
public class LibraryEndpoints : IEndpoints
{
public static void DefineEndpoints(IEndpointRouteBuilder app) { ... }
public static void AddServices(IServiceCollection services, IConfiguration configuration) { ... }
}- Tutte le API (
GET,POST,PUT,DELETE) sono mappate inDefineEndpoints. - I servizi richiesti (es.
IBookService) sono registrati inAddServices.
Esempio completo
public class LibraryEndpoints : IEndpoints
{
private const string ContentType = "application/json";
private const string Tag = "Books";
private const string BaseRoute = "books";
public static void DefineEndpoints(IEndpointRouteBuilder app)
{
app.MapPost(BaseRoute, CreateBookAsync)
.WithName("CreateBook")
.Accepts<Book>(ContentType)
.Produces<Book>(201).Produces<IEnumerable<ValidationFailure>>(400)
.WithTags(Tag);
app.MapGet(BaseRoute, GetAllBooksAsync)
.WithName("GetBooks")
.Produces<IEnumerable<Book>>(200)
.WithTags(Tag);
app.MapGet($"{BaseRoute}/{{isbn}}", GetBookByIsbnAsync)
.WithName("GetBook")
.Produces<Book>(200).Produces(404)
.WithTags(Tag);
app.MapPut($"{BaseRoute}/{{isbn}}", UpdateBookAsync)
.WithName("UpdateBook")
.Accepts<Book>(ContentType)
.Produces<Book>(200).Produces<IEnumerable<ValidationFailure>>(400)
.WithTags(Tag);
app.MapDelete($"{BaseRoute}/{{isbn}}", DeleteBookAsync)
.WithName("DeleteBook")
.Produces(204).Produces(404)
.WithTags(Tag);
}
internal static async Task<IResult> CreateBookAsync(
Book book, IBookService bookService, IValidator<Book> validator)
{
var validationResult = await validator.ValidateAsync(book);
if (!validationResult.IsValid)
{
return Results.BadRequest(validationResult.Errors);
}
var created = await bookService.CreateAsync(book);
if (!created)
{
return Results.BadRequest(new List<ValidationFailure>
{
new("Isbn", "A book with this ISBN-13 already exists")
});
}
return Results.Created($"/{BaseRoute}/{book.Isbn}", book);
}
internal static async Task<IResult> GetAllBooksAsync(
IBookService bookService, string? searchTerm)
{
if (searchTerm is not null && !string.IsNullOrWhiteSpace(searchTerm))
{
var matchedBooks = await bookService.SearchByTitleAsync(searchTerm);
return Results.Ok(matchedBooks);
}
var books = await bookService.GetAllAsync();
return Results.Ok(books);
}
internal static async Task<IResult> GetBookByIsbnAsync(
string isbn, IBookService bookService)
{
var book = await bookService.GetByIsbnAsync(isbn);
return book is not null ? Results.Ok(book) : Results.NotFound();
}
internal static async Task<IResult> UpdateBookAsync(
string isbn, Book book, IBookService bookService,
IValidator<Book> validator)
{
book.Isbn = isbn;
var validationResult = await validator.ValidateAsync(book);
if (!validationResult.IsValid)
{
return Results.BadRequest(validationResult.Errors);
}
var updated = await bookService.UpdateAsync(book);
return updated ? Results.Ok(book) : Results.NotFound();
}
internal static async Task<IResult> DeleteBookAsync(
string isbn, IBookService bookService)
{
var deleted = await bookService.DeleteAsync(isbn);
return deleted ? Results.NoContent() : Results.NotFound();
}
public static void AddServices(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IBookService, BookService>();
}
}3. Estensioni AddEndpoints e UseEndpoints
Queste extension methods permettono di richiamare automaticamente tutti gli endpoint registrati nel progetto:
builder.Services.AddEndpoints<Program>(builder.Configuration); // Service registration
app.UseEndpoints<Program>(); // Endpoint registration- Usa
typeof(TMarker)per cercare automaticamente tutte le classi che implementanoIEndpointsnellβassembly. - Esegue internamente i metodi
AddServiceseDefineEndpointsdi ciascuna classe.