Gestire le versioni di una asp.net Core WebAPI

Una delle più importanti ragioni percui le WebAPI hanno preso largamente piede negli ultimi anni è la possibilità di disaccoppiare fortemente la parte di rappresentazione da quella del layer dati/applicativo. Questo forte disaccoppiamento necessita però che cambi radicali alle WebAPI non vadano a discapito di chi le consuma: se cambio un API dovrei essere sicuro che una volta cambiata tutto ciò che prima funzionava continui a funzionare nella stessa maniera altrimenti potrei potenzialmente “rompere” delle funzionalità di applicazioni che consumano queste API. La maniera migliore per farla è quella di procedere ad un versionamento delle API, ma prima di farlo occorre capirsi sul quando è necessario creare una nuova versione delle API e quando no. Vi lascio questo link [1] che è ricco di spunti ed è ciò su cui ho basato questo post. Riassumendo le casistiche sarebbero più o meno le seguenti:

  • Rimuovere o rinominare API o i suoi parametri
  • Cambiamenti significativi nel comportamento dell’API
  • Cambiamenti al response contract
  • Cambiamenti ai codici di errore

Per prima cosa dobbiamo definire le versioni all’interno del Program.cs. In questo caso definiamo anche la version 1 come quella di default.

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1);
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-Api-Version"));
}).AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'V";
    options.SubstituteApiVersionInUrl = true;
});

Successivamente occorre decorare il controller con le Versioni supportate e con il conseguente path dinamico basato sulla versione

[ApiVersion(1)]
[ApiVersion(2)]
[Route("api/v{v:apiVersion}/[controller]")]
public class InfoAPIController : ControllerBase
{

A questo punto devono essere decorati appositamente tutti i metodi che hanno più versioni con lo stesso Http Get name ma differente nome C#

[MapToApiVersion(1)]
[HttpGet(Name = "GetInfo")] //, Authorize]
public string GetV1(string name)
{
    ...

[MapToApiVersion(2)]
[HttpGet(Name = "GetInfo")] //, Authorize]
public string GetV2(string name)
{

Fatto ciò dovremmo quindi essere in grado di usufruire versioni diverse in base al path utilizzato. In realtà, come spiegato per bene nel post sotto, le modalità potrebbero essere differenti ma io opto per un verisoning basato sull’url.

Tutto molto bello ma tutto ciò non basta a visualizzare due differenti versini in Swagger. Per farlo occorrono un altro paio di accortezze che ho scoperto in un altro post [2]. La prima è che vanno configurate le versioni visibili all’interno della configurazione di swagger (nel mio caso sono due):

builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Name = "authorization",
        Type = SecuritySchemeType.ApiKey
    });

    options.OperationFilter<SecurityRequirementsOperationFilter>();

    options.SwaggerDoc("v1", new OpenApiInfo { Title = "Xin Web API", Version = "v1"});
    options.SwaggerDoc("v2", new OpenApiInfo { Title = "Xin Web API", Version = "v2" });
});

Infine nel SwaggerUI vanno registrati i path delle versioni, ma invece di farlo uno ad uno consiglio di utilizzare l’approccio descritto qui [3]

    app.UseSwaggerUI(options =>
    {
        var descriptions = app.DescribeApiVersions();

        // Build a swagger endpoint for each discovered API version
        foreach (var description in descriptions)
        {
            var url = $"/swagger/{description.GroupName}/swagger.json";
            var name = description.GroupName.ToUpperInvariant();
            options.SwaggerEndpoint(url, name);
        }
    });

Attenzione che i due passi sopra sono fondamentali se volete visualizzare correttamente nella drop down di swagger netrambe le versioni e switchare tra di esse i due punti sopra sono fondamentali.

Swagger con le due versioni selezionabili.

[1] https://www.milanjovanovic.tech/blog/api-versioning-in-aspnetcore

[2] https://dev.to/sardarmudassaralikhan/swagger-implementation-in-aspnet-core-web-api-5a5a

[3] https://mohsen.es/api-versioning-and-swagger-in-asp-net-core-7-0-fe45f67d8419

Catalogo di Fumetti – La prima Web API con Swagger

Sin da piccolo sono sempre stato un grande amante di Fumetti. In effetti ne ho collezionati parecchi, principalmente Marvel, e mi piacerebbe avere una maniera facile di catalogarli e poterci accedere magari anche da dispositivi mobili. Proviamo quindi a creare un app per catalogarli e navigarli. Cominciamo con il definire la struttura dove salvare questi dati: una semplicissima tabella di database (SQL Server) come segue:

  • ID: Identificatore univoco
  • Series: la testata del fumetto
  • Title: il titolo del numero
  • Number: il numero del fumetto all’interno della testata

Partiamo quindi creandoci un catalogo vuoto e definiamo la tabella “Comic” come segue

Comic Table

Apriamo quindi VisualStudio 2019 IDE e creaiamo un nuovo progetto come “Web API Project”

ASP NET Core Web API Template

Chiamerò il progetto XinCataLogSwaggerWebAPI perchè voglio che l’API abbia anche l’interfaccia Swagger

Create Project

Come Framework identifico il 5.0 che è l’ultimo disponibile sulla mi macchina, per il momento lascio Authentication Type a None (ho visto parecchie esempi sul web e viene sempre lasciata a None nel caso servisse ci penseremo)

Framework and Authentication

A questo punto un progetto vuoto con la classe di esempio WeatherForecast è creato per mostrarne il buon funzionamento. Eseguiamo a questo punto il progetto e otteniamo:

Swagger API interface

Abbiamo la pagina Swagger per la nostra WebAPI, certo i metodi non sono che quello di default ma come inizio non è per nulla male.

Aggiungo il progetto a GitHub (lo potete trovare qui [1] se volete curiosarci). Ovviamente il suggerimento come sempre è avere un repository in cui poter salvare modifiche e recuperare i vostri errori nel caso li commettiate. Dato che dobbiamo utilizzare l’EntityFramework per implementare le CRUD da SQL Server aggiungiamo i seguenti pacchetti via NuGet (fate attenzione alla compatibilità con il vostro Framework)

EntityFrameworkCore 5.0.15

La lista finale di pacchetti che dobbiamo installare è la seguente:

NuGet Packages

Ricordo in passato di aver lavorato con un utilissimo wizard per generare il modello dati da un DB SQL con l’approcio Database First, sfortunatamente non sono riuscito a ritrovarlo e sembra che ciò sia dovuto al fatto che con .NET Core non si possono aggiungere oggetti ADO Entity Model [1]. Ad ogni modo è possibile sfruttare un comando dal Package Console Manager che fa qualcosa di similare:

Scaffold-DBContext "Server=WIN-7O15VR47QA6;Database=XinCataLog;Trusted_Connection=True;"  Microsoft.EntityFrameworkCore.SqlServer -OutputDir "Models" -Tables XinComic -f  

Il comando dice quale tabella/entità (in questo caso XinComic) su quale effettuare lo scaffold dell’oggetto e generare la classe model corrispettiva.

Package Manager Command
Models Folder

La classe XinCataLogContext è quella che effettua la comunicazione effettiva con il DB, mentre XinComic è il modello che rappresenta la tabella XinComic nel DB.

Ora passiamo a vedere come esporre le tipiche funzionalità che una API offre: i metodi HTTP di base sono normalmente 4 e si basano sulle operazioni CRUD (GET, PUT, POST, e DELETE vedi anche qui [2]).

  • GET ritorna la risorsa richiesta allo specifico URI. GET on ha effetti sul server (è una lettura).
  • PUT modifica una risorsa esistente allo specifico URI. PUT in alcuni casi può anche supportare la creazione se specificatamente prevista.
  • POST crea una nuova risorsa.
  • DELETE cancella una risorsa relativamente all’URI fornito.

Dato che noi abbiamo già creato un modello negli steps precedenti a cui associare le CRUD non serve fare altro che aggiungere un nuovo Controller alla folder Controller scegliendo specificatamente l’opzione sotto

API Controller with Actions using EntityFramework

Nella finestra modale che si apre non dobbiamo fare altro che scegliere le due classi che abbiamo creato allo step precedente come Model Class e Data Context Class

XinComicsController

Fatto ciò, dopo aver tritato per qualche secondo magicamente VisualStudio vi genererà il controller con tutti i metodi base funzionanti.

Devo dire che la prima volta che ho generato il controller mi sono beccato uno strano errore “Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly ‘Microsoft.VisualStudio.Web.CodeGeneration.Utils, Version=5.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60’. The system cannot find the file specified. File name: ‘Microsoft.VisualStudio.Web.CodeGeneration.Utils, Version=5.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60′” che ho risolto con il provvidenziale intervento di StackOverflow [4]. Qualora vi succedesse provateci, a me ha risolto il problema.

Ecco qui dunque il controller bello che pronto:

XinComicsController

In effetti se lanciamo ora il progetto i nuovi metodi sono presenti

XinComics methods

ma se ad esempio proviamo a lanciare la GET ci becchiamo un bel errore:

System.InvalidOperationException: Unable to resolve service for type 'XinCataLogSwaggerWebAPI.Models.XinCataLogContext' while attempting to activate 'XinCataLogSwaggerWebAPI.Controllers.XinComicsController'.

che ci dice in maniera abbastanza esplicita che abbiamo commesso qualche errore nell’inizializzazione del DB context XinCataLogContext. In effetti non lo abbiamo proprio registrato [5].

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<XinCataLogContext>(options =>
              options.UseSqlServer(Configuration.GetConnectionString("DefaultSQLServerConnection")));

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "XinCataLogSwaggerWebAPI", Version = "v1" });
            });
        }

La prima linea di codice del metodo registra il DB context che abbiamo creato prima. Adesso dobbiamo semplicemente referenziare la Connection String nel file appsettings.

  "ConnectionStrings": {
    "DefaultSQLServerConnection": "Server=WIN-7O15VR47QA6;Database=XinCataLog;Trusted_Connection=True;"
  }

E questo è tutto! Se ora lanciate l’applicazione scoprirete che i metodi funzionano come ci attendevamo. Abbiamo ottenuto la prima WebAPI (con Swagger) scrivendo al più un paio di linee di codice visto che la gran parte delle azioni le fa in toto Visual Studio That’s.

Get method from Database

[1] https://github.com/stepperxin/XinCataLogSwaggerWebAPI

[2] https://docs.microsoft.com/en-us/answers/questions/357012/can39t-find-adonet-entity-data-model-missing-visua.html

[3] https://docs.microsoft.com/en-us/aspnet/web-api/overview/older-versions/creating-a-web-api-that-supports-crud-operations

[4] https://stackoverflow.com/questions/45139243/asp-net-core-scaffolding-does-not-work-in-vs-2017

[5] https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro?view=aspnetcore-6.0#register-the-schoolcontext