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

Easy log per asp.net core: Serilog

E’ inutile spiegare perchè loggare sia un punto chiave nello sviluppo di un’applicazione. E’ altrettanto inutile dire quanto oggi sia inutile sviluppare un framework custom che lo faccia: ci sono mille plugin che lo fanno (e spesso anche molto bene) per cui c’è davvero l’imbarazzo della scelta. Non tutti però sono semplici da configurare (alcuni sono un vero incubo). La mia scelta dopo vari tentativi è ricaduta su SeriLog. Sul web trovate parecchia documentazione in merito ve ne suggerisco un paio sotto.

Nello specifico queste sono le azioni che ho condotto per installarlo e configurarlo:

  • Scaricare con NuGet Packages Manager il pacchetto Serilog.AspNetCore
  • Ho inizializzato il Logger all’interno del Program.cs
var logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .CreateLogger();

builder.Logging.ClearProviders();
builder.Logging.AddSerilog(logger);
  • Ho aggiunto nel file appsettings.js le configurazioni di scrittura, tra cui il nome del file dov’è posizionato etc…
"Serilog": {
  "Using": [ "Serilog.Sinks.File" ],
  "MinimumLevel": {
    "Default": "Information"
  },
  "WriteTo": [
    {
      "Name": "File",
      "Args": {
        "path": "../APIlogs/webapi-WebAPICoreAuthBearer.log",
        "rollingInterval": "Day",
        "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {Level:u3}] {Username} {Message:lj}{NewLine}{Exception}"
      }
    }
  ]
}

Con queste semplici azioni il vostro sistema già loggerà in automatico. Qualora vi servisse esplicitamente loggare all’interno dei vostri controller nel caso di un API naturalmente basta utilizzare la solita modalità “iniettiva”.

 private readonly ILogger<InfoAPIController> _logger;
 private readonly IConfiguration _configuration;

 public InfoAPIController(ILogger<InfoAPIController> logger, IConfiguration iConfig)
 {
     _logger = logger;
     _configuration = iConfig;
 }

 [HttpGet(Name = "GetInfo")] ]
 public InfoAPI Get()
 {
     _logger.Log(LogLevel.Information, "GetInfo");
....

[1] https://www.claudiobernasconi.ch/2022/01/28/how-to-use-serilog-in-asp-net-core-web-api/

[2] https://www.youtube.com/watch?v=QjO2Jac1uQw

[3] https://www.milanjovanovic.tech/blog/5-serilog-best-practices-for-better-structured-logging

Tuor e il suo arrivo a Gondolin

Questo che segue è un breve riassunto della vita Tuor dalla nascita fino al suo arrivo a Gondolin come narrato nei Racconti Incompiuti di JRR Tolkien. Il redatore del libro, chè è poi Christopher Tolkien (il figlio del grande autore) spiega che questo racconto in origine avrebbe dovuto essere “Tuor e la caduta di Gondolin” ma in realtà si fermerà all’entrata di Tuor a Gondolin.

Tuor: figlio di Huor e Rìan della casa di Hador

Ulmo: Signore delle Acque

Voronwe: Elfo

Al termine della Nirnaeth, Huor che al pari del fratello Hurin vi aveva partecipato combattendo al fianco degli Elfi non fa ritorno a casa cadendo sul campo di battaglia. Rìan sua madre, quando ne viene al corrente si lascia morire lasciando il piccolo Huor alle cure degli stessi Elfi. Tuor vi rimarrà fino a 16 anni, e viene a conoscenza del regno celato di Turgon, Re degli elfi, sfuggito a Morgoth nella disfatta della Nirnaeth. Turgon fù coperto nella fuga proprio dal padre Huor e dallo zio Hurin che come già detto pagaro con la propria vita. Da lì a poco gli Orchi attaccheranno ancora gli Elfi riuscendo a disperderli e farlo prigioniero. Tuor però non si darà pervinto e riuscirà con grande abilità a farsi tenere in vita fino a quando alla buona occasione, dopo tre anni, riesce a fuggire e ivi riceve un segno da parte di Ulmo che tramite un ruscello gli indica un passaggio. Il figlio di Huor da lì attraversa tutto il Dorlomin ed il Nevrast fino a giungere al monte Taras a ridosso del grande mare. Tuor entra dunque in Vinyamar l’antica città di pietra eretta dai Noldor. Molto bella è il racconto di quando entra nella sala del trono abbandonata da Turgon ed un segno (un altro) fa si che Tuor rinvenga spada e cotta di maglia lasciate dal re degli Elfi prima di partire: è una sorta di investitura. A questo punto mentre vine attirato sulle rive dalle acque gli appare Ulmo stesso che gli dà il compito di cercare Turgon ed il suo regno celato.

Mappa del percorso di Tuor fino al regno celato di Gondolin [1]

Prima di congedarlo Ulmo gli spiega i dettagli ed il perchè della missione donandogli un manto dal potere di nasconderlo alla luce ed allo sguardo e la promessa di mandargli qualcuno che lo possa condurre nel viaggio a quel posto segreto. Di lì a poco dalle acque emergerà il naufrago Voronwe ultimo superstite di una spedizione di 7 navi elfiche salpate dai Porti di Cìrdan con l’intento di far rotta verso Valinor e chiedere l’intervento dei Valar contro Morgoth. Nessuna di esse era più tornata e la nave di quest’ultimo era stata sferzata dalle tempeste lasciando lui come unico superstite. Tuor spiega della manifestazione di Ulmo e del messaggio che deve recare a Turgon e questo fa vincere l’iniziale ritrosia dell’elfo a mostrare il perscorso al regno celato ad un uomo. Partono quindi alla volta del regno celato. Il cammino è faticoso, tra il freddo gli stenti e i branchi di Orchi che braccano tutto ciò che si muove nelle zone. Tuor e Voronwe nel loro percorso devono mantenere il più stretto segreto, devono essere sicuri di non attirare attenzioni e portarsi dietro cattivi ospiti. Quando arrivano ai Monti Cerchianti, le mura del regime di Turgon, sono ormai allo stremo ma riescono a trovare la gola dove sfociava il Fiume Secco. Infine rinvengono una galleria scavata nella roccia in cui non vi è luce. Lì vengono intercettati da una pattuglia di guardiani Elfi che li identificano e si meravigliano del fatto che un Elfo abbia condotto un uomo per quel percorso, ma consì dell’eccezionalità delle persone che si trovano davanti decidono di scortarli fino da Turgon alchè sia lui a prendere ogni decisione in merito. Di lì vengono quindi condotti per una via angusta passando attraverso sette porte, una più fortificata dell’altra. Il racconto si interrompe dopo l’ultima porta alla visione di Gondolin innevata.

[1] http://lotrproject.com/map/beleriand/#zoom=3&lat=-861&lon=1500&layers=BTTTTTT

Aggiungere i Log in una Web API

Una delle cose fondamentali che serve per debuggare un applicazione sono i Logs. Avere un sistemi di log efficiente accorcia le tempistiche e favorisce un troubleshooting benfatto. In questo post mostro, brevemente, cosa si deve fare per utilizzare NLog a tal fine. Non voglio essere troppo noioso analizzando tutte le varie casistiche (nel caso vi consiglio questa lettura [1]) ma, voglio arrivare dritto al punto. Quello che a me serve è qualcosa che ad ogni eccezione venga correttamente loggata indipendentemente dal fatto che sia gestita e scriva in un file tutto quello che è successo.

A questo scopo installiamo i seguenti package NuGet:

Install-Package NLog.Web.AspNetCore
Install-Package NLog

Ed inseriremo nella root del progetto il seguente file config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
		  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		  autoReload="true"
		  internalLogLevel="Info"
		  internalLogFile="${basedir}\internal-nlog.txt">
		<!-- enable asp.net core layout renderers -->
		<extensions>
			<add assembly="NLog.Web.AspNetCore"/>
		</extensions>
		<variable name="basedir" value="${aspnet-appbasepath}\wwwroot\logs" />
		<targets>
			<target xsi:type="AsyncWrapper" name="AllAsyncWrapper" queueLimit="10000" batchSize="1000">
				<target xsi:type="File"
						name="allfile"
						fileName="${var:basedir}\nlog-all-${shortdate}-${environment:ASPNETCORE_ENVIRONMENT}.log"
						archiveFileName="${var:basedir}\archives\nlog-all-${shortdate}-${environment:ASPNETCORE_ENVIRONMENT}.archive-{#}.zip"
						archiveEvery="Day"
						maxArchiveDays="7"
						archiveNumbering="DateAndSequence"
						enableArchiveFileCompression="True"
						layout="${longdate}|${aspnet-traceidentifier}|${uppercase:${level}}|${threadid}|${logger}|${message} ${exception:format=ToString}|${aspnet-request-method}|${aspnet-request-url}|${aspnet-mvc-action}|${aspnet-request-posted-body}" />
			</target>
		</targets>
		<!-- rules to map from logger name to target -->
		<rules>
			<logger name="*" minlevel="Error" writeTo="AllAsyncWrapper" />
		</rules>
	</nlog>
</configuration>

Questo file fornisce le indicazioni su come comporre il file, dove metterlo come mantenerlo… Come detto non mi dilungo troppo ma vi pongo l’accento su un paio di punti:

<variable name="basedir" value="${aspnet-appbasepath}\wwwroot\logs" />

Questa riga sopra la utilizzo per definire come cartella dove salvare i files una cartella della www root, comoda se siete in una farm dove non avete controllo completo del file system. Per le Web API invece io uso questa dato che non esiste una wwwroot:

<variable name="basedir" value="${aspnet-appbasepath}\logs" />

Questa parte invece definisce tutte le proprità del file di log: da cosa deve contenere ed in che formato, alla dimensione massima, al nome, alla rotation… Insomma tutto quello che serve per meglio definire come loggare. Non dimenticate di flaggare il Copy del file nell’output.

				<target xsi:type="File"
						name="allfile"
						fileName="${var:basedir}\nlog-all-${shortdate}-${environment:ASPNETCORE_ENVIRONMENT}.log"
						archiveFileName="${var:basedir}\archives\nlog-all-${shortdate}-${environment:ASPNETCORE_ENVIRONMENT}.archive-{#}.zip"
						archiveEvery="Day"
						maxArchiveDays="7"
						archiveNumbering="DateAndSequence"
						enableArchiveFileCompression="True"
						layout="${longdate}|${aspnet-traceidentifier}|${uppercase:${level}}|${threadid}|${logger}|${message} ${exception:format=ToString}|${aspnet-request-method}|${aspnet-request-url}|${aspnet-mvc-action}|${aspnet-request-posted-body}" />

Fatto questo resta un unico punto: fare in modo che NLog venga correttamente lanciato nel Program.cs

	public class Program
	{
		public static void Main(string[] args)
		{
            var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try
            {
                logger.Debug("init main");
                CreateWebHostBuilder(args).Build().Run();
                //webhost.RunAsync();
            }
            catch (Exception exception)
            {
                //NLog: catch setup errors
                logger.Error(exception, "Stopped program because of exception");
                throw;
            }
            finally
            {
                // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
                NLog.LogManager.Shutdown();
            }

        }

        public static IHostBuilder CreateWebHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            logging.SetMinimumLevel(LogLevel.Trace);
        })
            .UseNLog();
    }

Un piccola nota: il codice del Main è strutturato per loggare l’errore nel caso l’inizializzazione non vada a buon fine.

A questo punto il più è fatto: lanciando l’applicazione è possibile trovare il logs nella cartella specificata:

File di log

Naturalmente è possibile utilizzare il logger in manier customizzata semplicemente iniettandolo all’interno del Controller e richiamandolo a piacere:

	public class HomeController : Controller
	{
		private readonly ILogger<HomeController> _logger;

		public HomeController(ILogger<HomeController> logger)
		{
			_logger = logger;
		}

		public IActionResult Index()
		{
			_logger.LogInformation("Home");
			return View();
		}

		public IActionResult Privacy()
		{
			return View();
		}

	}

[1] https://programmingcsharp.com/nlog-net-core/

Consumare una Web API in un APP MVC

Come ricorderete in uno dei miei post precedenti [1] ci eravamo divertiti a creare una Web API con Swagger UI per fornire delle classiche funzionalità CRUD ad un fantomatico database di Fumetti. Ovviamente quella modalità si può applicare a mille oggetti differenti ma, è ora è venuto il momento di consumare l’API in una vera applicazione MVC e vedere come orchestrare il tutto. Per questo esempio farò uso di un’altra API che esporrà un oggetto ServiceTable che, almeno nelle mie intenzioni dovrebbe indicare il tavolo di un locale. Questà entità è molto semplice:

  • ID: è l’identificativo univoco del tavolo (la PK sul DB)
  • Name: è il nome del tavolo, non è obbligatoria ma potrebbe essere utile a chi vuole dare dei nomi a tema… chessò Acqua, Terra, Mare, Fuoco…
  • Seats: è il numero di posti che ha il tavolo
  • Visible: qualora volessimo temporaneamente renderlo indisponibile.
Service Table API

Supponendo quindi di avere già questa API all’interno di un progetto chiamato XinCommonAPI dobbiamo creare la web app che consumerà questa API ed implementerà l’interfaccia grafica (la UI). Aggiungiamo dunque alla soluzione con il progetto contenente la WebAPI un nuovo Progetto

Add new Project

e scegliamo un ASP NET Core Web APP assicurandoci che utilizzi il paradigma MVC

Create MVC Project

Scegliamo quindi come al solito la cartella dove posizionarlo

Choose location

ed infine che tipo di framework vogliamo utilizzare

Framework and Authentication

Alla fine di tutto questo avrò ottenuto il mio nuovo progetto ASP NET Core pronto all’uso

Web App Project

Naturalmente come sempre, prima di fare qualsivoglia modifica il suggerimento è di verificare che i progetti funzioni ed, in questo caso, che funzionino entrambi. Infatti la Web App deve consumare la Web API e quindi entrambi i progetti devono essere lanciati in DEBUG. Per fare questo dobbiamo aprire le proprietà della Solution creata ed impostare entrambi i progetti su Start

Starting Project

Ed in effetti lanciati i due progetti mi ritrovo quello che mi attendevo:

Le due web app

Ok, ora passiamo all’implementazione dell’operazioni CRUD in MVC. Anzitutto io consiglio di crearci una ModelView che rappresenti l’entità che andrà a rappresentare (il nostro ServiceTable) praticamente riproducendo lo stesso tipo di proprietà esposte nell’API.

Creare una classe ModelView che rappresenti l’entità TableService

E questo sarà il codice che immetteremo

public class ServiceTableViewModel
	{
		public int Id { get; set; }
		public string Name { get; set; }
		public int? Seats { get; set; }
		public bool? Visible { get; set; }
	}

Ora passiamo a crearci un controller che vada a lavorare sulla Web API ServiceTable

Creare un Controller vuoto

Che chiameremo ServiceTableController.cs e che conterrà il seguente codice:

public class ServiceTableController : Controller
	{
		Uri baseAddress = new Uri("http://localhost:64853/api/FoodHut");
		HttpClient client;

		public ServiceTableController()
		{
			client = new HttpClient();
			client.BaseAddress = baseAddress;
		}

		public IActionResult Index()
		{
			List<ServiceTableViewModel> modelList = new List<ServiceTableViewModel>();
			HttpResponseMessage response = client.GetAsync(client.BaseAddress + "/ServiceTables").Result;
			if (response.IsSuccessStatusCode)
			{
				string data = response.Content.ReadAsStringAsync().Result;
				modelList = JsonConvert.DeserializeObject<List<ServiceTableViewModel>>(data);
			}
			return View(modelList);
		}
	}

Andiamo a veder nel dettaglio che cosa abbiamo aggiunto nel codice: anzitutto l’URL dell’API ovvero l’endpoint che andremo ad interrogare dove stanno le API. Per l’ambiente di debug lo trovate tra le properties del progetto

URI base of API

Questo Uri sarà quindi utilizzato per inizializzare l’oggetto HttpClient ed invocare la chiamata ottenendo la relativa response. Attenzione che essendo una chiamata Json il risultato va deserializzato facendo uso di un pacchetto Nuget specifico

Newtonsoft.Json library

Il codice in se per se è abbastanza autoesplicativo: si invoca l’API il risultato viene poi deserializzato e convertito nel ModelView relativo. Fate bene attenzione a che il ModelView riporti le properties con lo stesso identico name dell’API altrimenti il deserializzatore non sarà in grado di eseguire il mapping.

Fatto questo non resta che creare la view che visualizzi la lista dei ServiceTable e naturalmente come al solito la autogeneriamo posizionandoci sul metodo Index e quindi generiamo il tutto dal template List

List View

A questo punto è sufficiente lanciare in debug i due progetti ed ecco il risultato:

Lista dei ServiceTable creati

[1] https://www.beren.it/wp-admin/post.php?post=320&action=edit

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

XinLog 1.2 – Controlliamo i livelli di verbosità

Una delle feature più importanti quando si usa logga è la possibilità di definire il tipo di log che si sta scrivendo, questo perchè oltre ad essere una info assai importante per chi legge il log stesso ci consente anche di definire ad un livello più alto che dettaglio voglio avere nei stessi. L’esempio classico è quello della fase di sviluppo: normalmente quando si sta scrivendo un nuovo codice o script si ha necessità di avere un livello di log molto altro perchè serve per debuggare, ossia analizzare che cosa lo script ha eseguito passo-passo e magari magari il perché di un malfunzionamento o un evento inatteso. Al contrario, normalmente quando uno script è già stato utilizzato diverse volte non è necessario avere un dettaglio troppo elevato ma solo i messaggi più critici e le eccezioni. Detto questo per il nostro Log definiamo tre livelli principali:

  • Critical: per i problemi critici che hanno impatto nell’esecuzione dello script ad esempio gli errori
  • Warning: per problemi che non bloccano l’esecuzione ma per i quali è importante evidenziare l’esistenza
  • Info: per tutto ciò che può essere una info, ad esempio note per il debug
    Param (
        [Parameter(Mandatory)]    
        [string]$LogString,
        [ValidateSet("Info", "Warning", "Critical")]
        [string]$Level="Info"
    )

Aggiungiamo alla Write-Log il parametro $Level che definiremo tramite la funzione ValidateSet un LOV (List-of-Values) in modo che possano essere specificati solo i tre valori sopra e come valore di default imposto “Info” cosicchè se non specificato consideri il livello minore. A questo punto si tratta di modficare il log per aggiungere l’informazione del livello e dato che potrebbe essere utile definire anche un colore per la visualizzazione a schermo.

        $Stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
        $LogMessage = "$Stamp - $Level - $LogString"
        if($Level -eq "Info"){
            Add-content $global:_Logfile -value $LogMessage
            Write-Host $LogString -ForegroundColor White
        }
        elseif($Level -eq "Warning"){
            Add-content $global:_Logfile -value $LogMessage
            Write-Host $LogString -ForegroundColor Yellow
        }
        elseif($Level -eq "Critical"){
            Add-content $global:_Logfile -value $LogMessage
            Write-Host $LogString -ForegroundColor Red
        }

Ora che abbiamo modificato il log non ci resta che fare un test e verificare come cambia il log a seconda della tipologia che abbiamo scelto. Proviamo dunque con il test seguente:

Test d’utilizzo
Risultato a schermo
Risultato nei log

Come si può notare, se non specifichiamo il livello, di default viene interpretato come Info. Importante notare come un livello sconosciuto alzi un errore nel log, mentre il livello non è case sensitive.

Facciamo ora un altro step, normalmente quando si utilizzano i log è utile definire dall’applicazione che li utilizza che livello si vuole loggare in modo da evitare un livello troppo “verboso” qualora non sia necessario. Per farlo aggiungiamo nella Open-Log la possibilità di definire il livello di log

function Open-Log{
    Param (   
        [string]$StoragePath,
        [ValidateSet("Info", "Warning", "Critical")]
        [string]$Level="Info"
    )
    try{
        $global:_Level = $Level
        if($StoragePath -ne $null){
       ...

Naturalmente come fatto in precedenza memorizziamo l’informazione sul livello scelto in una variabile globale di modo che resti disponibile durante tutto il processo. A questo punto si tratta di aggiungere il check all’interno della Write-Log che scriverà solo ciò che rispetta il criterio di log definito nella Open-Log.

        $Stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
        $LogMessage = "$Stamp - $Level - $LogString"
        if(($Level -eq "Info") -and ($global:_Level -eq "Info")){
            Add-content $LogFile -value $LogMessage
            Write-Host $LogString -ForegroundColor White
        }
        elseif(($Level -eq "Warning") -and (($global:_Level -eq "Warning") -or ($global:_Level -eq "Info"))){
            Add-content $LogFile -value $LogMessage
            Write-Host $LogString -ForegroundColor Yellow
        }
        elseif($Level -eq "Critical"){
            Add-content $LogFile -value $LogMessage
            Write-Host $LogString -ForegroundColor Red
        }

Ora se facciamo girare lo stesso test di prima definendo come livello minimo Warning tutte le Info non verranno riportate come voluto

Test con livello di verbosità su Warning

Bene, direi che, almeno per il momento il livello di flessibilità raggiunto dallo XinLog dovrebbe essere sufficiente. Se volete scaricare tutto il sorgente lo trovate qui [1].

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

Moby Dick

Mi sono sempre ben visto dal cominciare a leggere Moby Dick per quell’alone da grande classico / mattone che ha sempre avuto. Poi quest’estate, in un impeto di letterario, l’ho comprato e soprattutto l’ho messo in cima alla lista dei libri da leggere. In realtà è più uno scaffale: io ho uno scaffale dove tengo solo i libri da leggere in un ordine, più o meno, di lettura consigliata. Si lo so, probabilmente ce l’hanno tutti ma era per precisare. Dopo quasi 5 mesi in cui più di una volta stavo per chiuderlo per sempre mi sono fatto forza e finalmente oggi l’ho finito. Embè? Direte voi… E’ proprio un mattone difficile da digerire. Direi che più che un romanzo è una sorta di trattato su balene e baleniere con tanto di dettagli scientifici, sociali, storici, geografici, linguistici… Davvero un lavoro enorme e articolato, ed è forse proprio per questo che mi ha reso la vita difficile. In effetti io mi attendevo un libro di grande avventura ed adrenalina con quel pazzo capitano Achab alla forsennata ricerca di fare la pelle al diabolico cetaceo che gli aveva avvelenato l’esistenza. Intendiamoci eh, c’è anche quello, anche se tutto sommato nelle sole ultime 50 pagine. Tutto il resto è una lenta preparazione in cui vengono fatte dissertazioni su tutte le specie di balene esistenti, e non avevo idea di quante potessero essere, di come le si caccino, di come una volta uccise vengano lavorate ed estratto il grasso di balena. Davvero un’istantanea di grande pregio sulle baleniere di metà ottocento e sul lavoro di chi le governava spesso tra mille difficoltà e problemi che noi oggi nemmeno ci possiamo immaginare. Se poi a tutto ciò uniamo uno stile letterario estremamente forbito e a volte eccessivamente tecnico, purtroppo per chi come me non ha grosse conoscenze letterarie e legge più che altro come passatempo, l’impresa diventa più ardita di quella dello stesso Achab.

Insomma, in fin dei conti non lo sconsiglio, perchè senza dubbio è un’opera di grande rilievo, ma vi metto in guardia: Moby Dick viene nominato la prima volta a pagina 200, sapete cosa vi attende…

XinLog 1.1 – Rendiamolo configurabile

Ormai ci ho preso gusto dopo le ultime puntate continuo a lavorare sul migliorare lo script di Log. Per cominciare vorrei dare l’opportunità a chi lo utilizza di decidere dove loggare. Come ricordate infatti al momento tutti i logs finiscono nella stessa cartella in cui c’è lo script e questo è limitante. Sarebbe figo invece che chi lo usa possa decidere di volta in volta dove loggare. Creeremo dunque una funzione Open-Log a cui dire come inizializzare dei parametri che poi si applichino.

# Get the current Directory
$_StoragePath = Split-Path -Parent $MyInvocation.MyCommand.Path
#Set the file log name
$_Logfile = "_StoragePath\XinLog_$($env:computername)_$((Get-Date).toString("yyyyMMdd_HHmmss")).log"

function Open-Log{
    Param (   
        [string]$StoragePath
    )
    #set the folder name
    $_StoragePath = $StoragePath
    #Set the file log name
    $_Logfile = "$_StoragePath\XinLog_$($env:computername)_$((Get-Date).toString("yyyyMMdd_HHmmss")).log"
}

Se invochiamo quindi questa Open-Log prima di utilizzarla, l’idea è che si inizializzi la variabile $_StoragePath e da lì in poi essa venga richiamata ogni volta nella Write-Log. Se però lo fate scoprirete che non funziona. Perchè? In pratica per come sono dichiarate le variabili sopra hanno un contesto limitato allo script che quindi cessa di esistere aldifuori del fil XinLog. Questo ovviamente non ci piace perchè se dobbiamo inizializzare ogni volta il file tutto perde di senso.

E’ il caso invece di giocare un pochino con le variabili e qui [1] trovate un articolo interessante in merito. E’ il caso che dichiari Global queste variabili così da riuscire a mantenere il valore nel contesto del thread lanciato e non solo del singolo file XinLog: Funziona!

# Get the current Directory
$global:_StoragePath = Split-Path -Parent $MyInvocation.MyCommand.Path
#Set the file log name
$global:_Logfile = "$global:_StoragePath\XinLog_$($env:computername)_$((Get-Date).toString("yyyyMMdd_HHmmss")).log"

function Open-Log{
    Param (   
        [string]$StoragePath
    )
    #set the folder name
    $global:_StoragePath = $StoragePath
    #Set the file log name
    $global:_Logfile = "$global:_StoragePath\XinLog_$($env:computername)_$((Get-Date).toString("yyyyMMdd_HHmmss")).log"
}

Come potete ben vedere l’unica accortezza è di utilizzare il prefisso $global: per tutte le variabili che vogliamo mantengano un valore che esuli dal singolo contesto di File e persistano anche nel Thread.

Infine aggiungiamo un po’ di try-catch per evitare che log (utilizzato anche per loggare gli errori) possa a sua volta bloccarsi per qualche eccezzione inattesa ed il gioco è fatto: la versione XinLog 1.1 è servita (GitHub [2])

[1] https://www.varonis.com/blog/powershell-variable-scope

[2] https://github.com/stepperxin/XinLog