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 parametroage
deve essere un intero.
β Viene automaticamente convertito inint age
nel metodo handler. - Espressione Regolare:
"{carId:regex(^[a-z0-9]+$)}"
βcarId
deve 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)}"
βisbn
deve 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 queryParam
Il 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 guidGenerator
Qui 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 encoding
Il 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
TryParse
adatto 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
Book
nel 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 default
Oppure 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:
AddEndpoints
eUseEndpoints
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 implementanoIEndpoints
nellβassembly. - Esegue internamente i metodi
AddServices
eDefineEndpoints
di ciascuna classe.