Creare una Web API con ASP.NET Core

Creare una Web API con ASP.NET Core

Uno degli usi più diffusi di C# è la costruzione di servizi web e API. ASP.NET Core è il framework di Microsoft per lo sviluppo web multipiattaforma: veloce, modulare e progettato attorno alla dependency injection che abbiamo appena studiato. In questo articolo costruiamo una semplice Web API REST, esplorando il routing, i verbi HTTP, il binding dei dati e la restituzione delle risposte. L'obiettivo è capire come i concetti visti finora si combinino in un'applicazione reale.

Creare il progetto

Come per l'applicazione console della prima serie, si parte da un template. Il template webapi genera un progetto Web API già configurato con l'infrastruttura essenziale.

dotnet new webapi -o ProductApi
cd ProductApi
dotnet run

Il punto di avvio

Il file Program.cs è il cuore della configurazione. Qui si costruisce l'applicazione, si registrano i servizi nel contenitore di dependency injection e si definisce la pipeline che elabora le richieste. La struttura moderna, basata sui top-level statements, rende il tutto conciso.

var builder = WebApplication.CreateBuilder(args);

// Registrazione dei servizi nel contenitore di DI
builder.Services.AddControllers();
builder.Services.AddSingleton<IProductRepository, InMemoryProductRepository>();

var app = builder.Build();

// Configurazione della pipeline delle richieste
app.UseHttpsRedirection();
app.MapControllers();

app.Run();

Il modello dei dati

Un'API espone e riceve dati. Definiamo una semplice classe che rappresenta un prodotto: sarà l'entità che i nostri endpoint restituiranno e accetteranno, serializzata automaticamente in formato JSON.

class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public decimal Price { get; set; }
}

I controller

Un controller raggruppa gli endpoint correlati a una risorsa. Deriva da ControllerBase e usa attributi per definire il routing e i verbi HTTP. Grazie alla dependency injection, il repository di cui il controller ha bisogno viene iniettato automaticamente nel costruttore, esattamente come abbiamo visto nell'articolo precedente.

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;

    // Il repository e iniettato dal contenitore di DI
    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }
}

L'attributo ApiController abilita comportamenti utili come la validazione automatica del modello, mentre Route definisce il percorso base; il segnaposto [controller] viene sostituito dal nome del controller senza il suffisso, producendo qui il percorso api/products.

Leggere i dati con GET

Il verbo HTTP GET recupera le risorse. Definiamo due endpoint: uno che restituisce l'elenco completo e uno che restituisce un singolo prodotto dato il suo identificatore, letto dal percorso dell'URL.

[HttpGet]
public IEnumerable<Product> GetAll()
{
    return _repository.GetAll();
}

[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
    Product? product = _repository.GetById(id);

    if (product == null)
    {
        // Restituisce lo stato HTTP 404 Not Found
        return NotFound();
    }

    // Restituisce lo stato 200 OK con il prodotto serializzato in JSON
    return Ok(product);
}

Il tipo di ritorno ActionResult<Product> è flessibile: consente di restituire sia i dati sia un codice di stato HTTP appropriato. Metodi helper come Ok e NotFound producono le risposte con lo stato corretto.

Creare risorse con POST

Il verbo POST crea nuove risorse. Il corpo della richiesta, in formato JSON, viene deserializzato automaticamente nel parametro grazie al model binding, segnalato dall'attributo FromBody. La convenzione REST prevede di rispondere con lo stato 201 Created e l'indirizzo della risorsa appena creata.

[HttpPost]
public ActionResult<Product> Create([FromBody] Product product)
{
    _repository.Add(product);

    // Stato 201 Created con il percorso della nuova risorsa
    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}

Aggiornare ed eliminare

Completiamo le operazioni CRUD con i verbi PUT, per l'aggiornamento, e DELETE, per la rimozione. Entrambi combinano un parametro dal percorso con, nel caso di PUT, il corpo della richiesta.

[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] Product product)
{
    if (_repository.GetById(id) == null)
    {
        return NotFound();
    }

    _repository.Update(id, product);

    // Stato 204 No Content: operazione riuscita senza corpo di risposta
    return NoContent();
}

[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
    if (_repository.GetById(id) == null)
    {
        return NotFound();
    }

    _repository.Delete(id);
    return NoContent();
}

Le minimal API

Per API semplici, ASP.NET Core offre un'alternativa ancora più snella: le minimal API, che permettono di definire gli endpoint direttamente nel file di avvio, senza controller. Sono ideali per microservizi e prototipi, e mostrano bene la flessibilità del framework.

var app = builder.Build();

// Definizione di un endpoint direttamente, senza controller
app.MapGet("/api/products", (IProductRepository repo) => repo.GetAll());

app.MapGet("/api/products/{id}", (int id, IProductRepository repo) =>
{
    var product = repo.GetById(id);
    return product == null ? Results.NotFound() : Results.Ok(product);
});

app.Run();

Anche nelle minimal API la dependency injection funziona: i servizi richiesti vengono iniettati come parametri della funzione che gestisce l'endpoint.

La validazione del modello

Grazie all'attributo ApiController, se il modello ricevuto non è valido, il framework risponde automaticamente con lo stato 400 Bad Request. È possibile arricchire il modello con attributi di validazione che descrivono i vincoli, come l'obbligatorietà o gli intervalli ammessi.

using System.ComponentModel.DataAnnotations;

class Product
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public required string Name { get; set; }

    [Range(0, 100000)]
    public decimal Price { get; set; }
}

Conclusione

Abbiamo costruito una Web API con ASP.NET Core, partendo dal template e configurando il punto di avvio con la registrazione dei servizi nel contenitore di dependency injection. Abbiamo implementato le operazioni CRUD tramite i verbi HTTP GET, POST, PUT e DELETE, restituendo codici di stato appropriati, sfruttato il model binding e la validazione, e visto l'alternativa delle minimal API. I dati, però, vivevano finora in memoria: nel prossimo articolo li renderemo persistenti con Entity Framework Core, l'ORM di riferimento per .NET.