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

Gestire differenti Appsettings in base all’ambiente di rilascio

Quando si lavora su una soluzione è vitale che si abbia la possibilità di differenziare delle configurazioni in base all’ambiente di destinazione “target” della soluzione. L’esempio più semplice è quello delle stringhe di connessione a DB: se si hanno ambienti diversi normalmente serviranno delle stringhe differenti in base al DB. In generale per i progetti asp.net core (e non solo) è possibile gestire tanti settings quanti sono gli ambienti di rilascio, però non è così banale trovare un modo per rendere dinamica questa modalità. Ho travato infatti molta documentazione sul come giocare con le variabili di ambiente [1] che però, nell’ambiente target (una soluzione cloud), non mi è possibile toccare. Dopo varie ore a cercare in rete qualcosa di sensato sono approdato a questa soluzione che vi spiego che in parte trovate anche in questo video.

Definizione di ambienti di rilascio: per prima cosa definiamo quali ambienti deve contemplare la mia soluzione per capire quanti varianti di file di configurazione servono. Nel mio caso sono 4:

  • Development: la configurazione che utilizzo in Visual Studio quando sviluppo e debuggo
  • Stage: la configurazione che utilizzo per testare l’applicazione sul’ISS locale della macchina di sviluppo
  • Sandbox: l’ambiente di preproduzione in cloud
  • Live: l’ambiente di produzione reale

Per ognuno di questi ambienti mi servirà un file di appsettings dedicato formattato nella seguente maniera: appsettings.{env}.json. Per farlo basta copiare il file appsettings già presente nella soluzione e rinominarlo utilizzando i quattro nomi sopra. Tenete sempre in conto che il primo file ad essere letto è appsettings (quello generico) che poi verrà sovrascritto da quello con il nome dell’ambiente. Questo significa che tutto ciò che chiede di essere specifico per ambiente deve finire in nel file con il nome dell’ambiente stesso.

Caricamento dei settings corretti: in Program.cs carichiamo anzitutto il file appsettings generico all’interno del quale andiamo a creare una configurazione che identifichiamo con Configuration dove scriveremo il target del deploy (uno dei 4 valori sopra). Ed in base a quel valore andiamo a caricare il file dedicato.

var _conf = builder.Configuration.AddJsonFile("appsettings.json", optional: true, false).Build();
string _env = _conf.GetSection("Configuration").Value;
builder.Configuration.AddJsonFile($"appsettings.{_env}.json", optional: true, false);

var app = builder.Build();

In questa maniera basterà identificare nel appsettings generico il target del deploy (volendo anche al volo) all’interno della variabile di configurazione Configuration.

Rilasciare solo i files dell’ambiente: per come fatto sopra e sostanzialmente mostrato anche nel video tutti i files appsettings verranno sempre deliverati in tutti gli ambienti e la cosa non mi mpiace molto perchè si presta ad avere errori se non modifico correttamente il Configuration all’interno dell’appsettings generico. Per ovviare a questo problema genero 3 nuove versioni dal configuration manager: Live, Sandbox e Stage. A questo punto apro il file di progetto in edit ed aggiungo la seguente configurazione che rilascia solo il file corretto in base al target che ho scelto.

	<Choose>
		<When Condition="'$(Configuration)' == 'Live'">
			<ItemGroup>
				<None Include="appsettings.Live.json" CopyToOutputDirectory="Always" CopyToPublishDirectory="Always" />
				<None Include="appsettings.json" CopyToOutputDirectory="Never" CopyToPublishDirectory="Never" />
				<Content Remove="appsettings.*.json;appsettings.json" />
			</ItemGroup>
		</When>
		<When Condition="'$(Configuration)' == 'Sandbox'">
			<ItemGroup>
				<None Include="appsettings.Sandbox.json" CopyToOutputDirectory="Always" CopyToPublishDirectory="Always" />
				<None Include="appsettings.json" CopyToOutputDirectory="Never" CopyToPublishDirectory="Never" />
				<Content Remove="appsettings.*.json;appsettings.json" />
			</ItemGroup>
		</When>
		<When Condition="'$(Configuration)' == 'Stage'">
			<ItemGroup>
				<None Include="appsettings.Stage.json" CopyToOutputDirectory="Always" CopyToPublishDirectory="Always" />
				<None Include="appsettings.json" CopyToOutputDirectory="Never" CopyToPublishDirectory="Never" />
				<Content Remove="appsettings.*.json;appsettings.json" />
			</ItemGroup>
		</When>
		<Otherwise>
			<ItemGroup>
				<None Include="appsettings.Development.json" CopyToOutputDirectory="Always" CopyToPublishDirectory="Always" />
				<None Include="appsettings.json" CopyToOutputDirectory="Never" CopyToPublishDirectory="Never" />
				<Content Remove="appsettings.*.json;appsettings.json" />
			</ItemGroup>
		</Otherwise>
	</Choose>

In questo modo basterà prima di rilasciare in uno degli ambienti selezionare la tipologia di deploy e verranno solo rilasciati i files di configurazione relativi.

[1] https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0#evcp