Acquisire testo da immagini con Python – Parte 2 – Tesseract e il primo test di acquisizione

Nel precedente post al fine di provare ad acquisire e aggregare dati provenienti da schermate di un video gioco anni novanta abbiamo chiesto a ChatGPT di darci una mano nel compito essendo neofiti totali. La scorsa volta ci siamo fermati all’installazione di Python, ora passiamo a Tesseract.

Tesseract

Se proviamo a far girare il codice che ci ha fornito ChatGPT scopriamo che manca un prerequisito che è Tesseract. Ma cos’è esattamente?

Ecco la risposta sempre di ChatGPT

La risposta di ChatGPT

Bene, Tesseract è un OCR ed è utilizzato per estrarre testi dalle immagini: quello che mi serve. E’ Open-Source, supporta il riconoscimento in varie lingue ed è molto accurato se correttamente “allenato” (interessante). Può essere facilmente utilizzato attraverso API e nello sopecifico per sessere tuilizzato in Python necessita della libra “pytesseract”. Direi che è esattamente quello che mi serve. Per installare Tesseract basta un semplice comando con brew [1]

brew install tesseract

Inifine come suggerito da ChatGPT installo anche il wrapper per Python.

pip install pytesseract

A questo punto possiamo cominciare a lavorare sul codice Python per capire come adattarlo e ricondurlo a quelle che sono le mie necessità.

Primo ciclo di codice

Apriamo Visual Studio Code e creiamo un file vuoto Test.py e copiamo il codice suggerito nel post precedente quindi lanciamo l’esecuzione dal menu Run > Start Debugging. Questo è il risultato:

Primo lancio

L’esecuzione va in errore e la modalità debug di Visual Studio Code ci aiuta evidenziando dove sta il problema: certo devo fornire un path corretto dove prelevare gli screenshots. Al netto di questo errore comunque il setup sembra corretto possiamo quindi dedicarci alla parte più divertene: vale a dire scrivere il codice. Anzitutto faccio un po’ di pulizia: rimuovo la parte che fa la categorizzazione perchè al momento non so ancora come poterla implementare e lo stesso faccio con la funzione che scrive il csv. Infine fornisco il path dove ho già preparato alcuni screenshots da cui estrarre il testo che mi serve. Il main dopo questo restyling è molto minimale:

# Main function to execute the workflow
def main():
    # Path to the folder containing screenshots
    screenshot_folder = ""/Users/xxxx/ScreenCapture""

    # Extract data from screenshots and rename files
    extract_data_and_rename(screenshot_folder)

Infine mi dedico alla funzione principale extract_data_and_rename che chiaramente itera i files nella cartella e tramite pytesseract estrae il testo dell’immagine. Al momento però mi limito a fare un print del dato estratto:

# Function to extract data from screenshots and rename files
def extract_data_and_rename(screenshot_folder):
    for file in os.listdir(screenshot_folder):
        if file.endswith(".png"):
            img_path = os.path.join(screenshot_folder, file)
            img = cv2.imread(img_path)
            # Implement OCR to extract data from the image
            extracted_data = pytesseract.image_to_string(img)
            # Extract relevant information from the data
            print(extracted_data)

Ok ci siamo se lo lanciamo teoricamente dovrebbe iterare tutti i files png presenti nella cartella e scrivere il contenuto estratto da ognuno di essi a schermo. Per questa prima prova uso una sola immagine:

Immagine sorgente

e questo è ciò che il sistema è stato in grado di interpretare:

Testi estratta dallo screenshot

Beh, diciamo che come primo test è già qualcosa però è evidente che alcuni testi sono stati correttamente interpretati mentre altri vanno rivisti. C’è parecchio da lavorare!

[1] https://pyimagesearch.com/2021/08/16/installing-tesseract-pytesseract-and-python-ocr-packages-on-your-system/

Creare un Component in React.js

Per component (componente) normalmente si fa riferimento a qualcosa che può essere utilizzato/riutilizzato in diversi contesti normalmente in associazionne con altri componenti. Nel contesto di React.js i component sono oggetti che ritornano sostanzialmente del HTML e sono codificati in files .js. Sono normalmente messi in una cartella “components” nel folder src. Nel caso che vedete sotto andiamo a creare un component Employee e lo posizionamo in quel folder. Se vi interessa approfondire la modalità con cui si linkano le funzionalità tra diversi file js vi rimando a questo post [1].

Cartella components

A questo punto “disegnamo” html direttamente nel componente. Si, in soldoni l’idea è che il componente ritorni il frammento dell’HTML che poi verrà renderizzato da chi il componente lo richiama.

function Employee(){
    return <h3>Here is an employe</h3>;
}

export default Employee;

Nell’esempio disegneremo un titolo h3 e lo incorporeremo nella App.js

import './App.css';
import Employee from './components/Employee'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Employee/>
        ...
      </header>
    </div>
  );
}

export default App;

Con questo esito:

application display component

Come potete notare è fondamentale che la function nel return ritorni un frammento di HTML che sia totalmente contenuto in un unico tag. Questo deve succedere sempre a partire da App.js. All’interno della function e prima del return si possono utilizzare tutti i comandi classici javascript ed implementare logiche, anche complesse. Immaginiamo che per qualche ragione si debbano visualizzare dei tag o delle info piuttosto che altre, in base a delle logiche definite. Una possibilità potrebbe essere quella di creare una variabile ed in base al suo valore decidere cosa renderizzare a schermo.

E’ possibile infatti utilizzare delle variabili all’interno del jsx che viene ritornato dal componente stesso e per farlo basta utilizzare le parentesi graffe {}.

      <header className="App-header">
        { returnEmployees ?
          <>
            <Employee/>
            <Employee/>
            <Employee/>
          </>
          :
          <></>
        }
      </header>

Nell’esempio riportato sopra si usa un’operatore ternario per verificare la variabile e ritornare il frammento che serve in base all’esito di quella verifica. Importante è che l’output sia sempre contentuto in un tag (anche quello vuoto) come nell’esempio. Si possono aggiungere più frammenti {} anche uno di seguito all’altro ma non vanno innestati. Importante: all’interno di questi frammenti si utilizza la sintassi javascript.

E’ bene infine ribadire che un component non è necessario che sappia nulla del contesto in cui è utilizzato perchè deve potenzialmente essere in grado di esistere così com’è. E’ solo chi utilizza che deve conoscere come funziona e come utilizzarlo correttamente.

[1] https://www.beren.it/2024/02/02/import-export-and-export-default-in-javascript/

Import, Export and Export Default in Javascript

Lavorando con diversi javascripts files nell’intenzione di separare quanto più possibile il codice e comunque riuscire a sfruttare funzionalità contenute in alcuni files in altri files è molto utile capire come funzionano Import ed Export.

La keyword export serve proprio a rendere disponibile all’esterno di un file una parte di codice definita nel file stesso. Ci sono due modalità con cui farlo: l’export e l’export default.

Export Default: è normalmente utilizzato quando si vuole condivider una specifica parte di codice all’esternoe le keywords da apporre sono export default

// 📂 math.js
const add = (a, b) => a + b;
export default add;

// 📂 main.js
import myAddFunction from './math.js';
const result = myAddFunction(5, 10); // This will call the add function from math.js and store the result in the 'result' variable.

Nell’esempio vediamo come la funziona add esportata nel primo file possa essere utilizza semplicemente importando il file stesso nel file che la vuole utilizzare.

Export: questa modalità è utile quando si vogliono esportare più pezzi di codice dallo stesso file ed in questo caso la keyword da utilizzare sarà solo export

// 📂 math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// 📂 main.js
import { add, subtract } from './math.js';

const result1 = add(5, 3); // result1 will be 8
const result2 = subtract(10, 4); // result2 will be 6

Import: è invece la keyword che serve ad importare le funzionalità esposte dai files esterni tramite le export e renderle fruibili al contesto del file corrente.

Riassiumiamo qui sotto le prinicpali caratteristiche di entrambi:

  • Con l’export semplice quando usiamo l’import dobbiamo esplicitamente definire thra parentesi graffe {} quali sono le funzionalità che vogliamo utilizzare e dobbiamo anche fare uso del nome esatto esposto. Con l’export default questo non serve.
  • All’interno di un file ci possono essere molteplici export ma solo un export default
  • export e export default possono essere utilizzati all’interno dello stesso file

Per chi volesse approfondire vi lascio un link [1] da cui ho tratto questi spunti.

[1] https://www.freecodecamp.org/news/difference-between-default-and-named-exports-in-javascript/#:~:text=In%20JavaScript%2C%20a%20default%20export,using%20the%20export%20default%20syntax.

Configurare GitHub in una React App con Visual Studio Code

Per chi lavora al codice è importante poter aver repository di codice che consenta principalmente due cose:

  • La gestione del codice in maniera concorrente: tipicamente più sviluppatori possono aver necessità di lavorare sugli stessi files, senza ostacolarsi e senza che l’uno “distrugga” quanto fa l’altro
  • La gestione delle versioni: avere la possibilità di tenere più versioni dello stesso file basate sul quando il file stesso è stato “salvato” nel repository consentendo rapidamente di individuare possibile problemi introdotti ad esempio con un cambiamento sull’ultima versione e poter riprisitnare una versione qualsiasi precedentemente salvata.

In questo post utilizzeremo come repository GitHub, che è del tutto free e consente agli utenti registrati di avere un repository on cloud sempre disponibile ed anche condivisibile. Assumendo di avere già un account creiamo online un nuovo repository (il nome lo potete scegliere a piacere) evitando di inizializzarlo dato che partiamo da una app react creata localmente (date un’occhiata a questo post [1] se non sapete come farne una).

GitHub – Create Repository

Una volta cliccato su Create Repository viene mostrata una pagina di configurazione in cui sono indicati i comandi da lanciare in console per creare e pushare il codice sul repostory appena creato. Basta copiare, incollare nel terminale e cliccare su invio.

GitHub – Repository Commands

A questo punto nella schermata di Visual Studio Code nel tab del versioning appaiono tutti i files della soluzioni che devono essere aggiunti.

Repository files list

I files hanno ora tutti una U sulla destra ma che significa esattamente?

  • U (Untracked): sono files aggiunti al progetto ma non ancora commitatti e nemmeno aggiunti all astaging area
  • M (Modified): un file presente nel repository e modificato
  • A (Added): un file aggiunto alla staging area
  • D (deleted): un file tracciato nel repository e cancellato localmente
  • C (Conflict): un file che ha un conflitto di versione

Quando parliamo di staging area definiamo quei files che vanno a formare un commit e che non sono ancora presenti nel repository.

Ora non resta che scrivere un commento nel box e cliccare commit. Quindi premere push per sincronizzare il tutto. Il risultato è che ora in GitHub nel repository indicato ho tutti i files del mio progetto locale.

Respository load in GitHub

Un occhio attento però noterà che non tutti i files che sono presenti nella cartella locale del progetto sono stati importati. Ad esempio build o node_modules non sono presenti. In realtà ciò è corretto ed il perchè è definito all’interno del file .gitignore posso definire quali file ignorare perchè superflui. Nella fattispecie build contiene la pubblicazione del sito mentre node_module ritorna i pacchetti utilizzati per la build.

[1] https://www.beren.it/2024/01/19/la-mia-prima-react-app/

La mia prima React App

React è una libreria Javascript tra le più utilizzate nell’implementazione di interfacce web [1]. E’ molto utilizzata per chi lavora con le Sigle Page Application in cui buona sostanza a differenza delle standard web application il DOM viene riscritto per intero ad ogni interazione lasciando al browser l’onere di ridisegnarla in base alla navigazione utente. E’ una modalità che ben si sposa con il concetto di Headless e di architettura composable.

Per prima cosa bisogna installare node.js sulla macchina dove si prevede di sviluppare l’applicazione. A questo punto possiamo utilizzare un comando specifico da terminale per creare l’app: create-react-app. Questo comando lancia la creazione dell’applicazione nella folder corrente del terminale quindi fate attenzione a quando lanciate il comando.

npx create-react-app hello
Crazione della React App

Al termine di un processo che può durare alcuni minuti questo comando genererà la struttura di files che vanno a comporre l’applicazione e che è ben descritta qui [2].

Abbiamo creato il template di app, ora ci serve aprire il tutto in editor di testo dove ci venga facile fare modifiche e lavorare sui files. Ci sono decine di editor ma quello che vi consiglio è Visual Studio Code (lo trovate qui [3]), è free, è leggero ed ha un sacco di estensioni utili. Vale la pena porre attenzione sui due files principali:

Entry Point

Questi files devono essere sempre presenti perchè sono sorgente da cui parte tutto. Index.html in particolare ha in div con id=”root” dal quale parte tutto: è all’interno di questo div che verranno generati di volta in volta i componenti da visualizzare nella pagina.

Index.html

Questo compito verrà svolto da index.js che come vedete sotto ricercherà quel did e lo sostituirà con qualcosaltro che la soluzione definisce, in questo caso <App /> che non è nientaltro che tutto ciò che si trova nel file App.js prsente sempre nella cartella src.

index.js

A questo punto per vedere l’app in azione non ci resta che aprire un nuovo Terminal dal menu di Visual Studio Code ed eseguire il comando:

npm start

Ed eccoci quà l’applicazione demo funziona:

Quando invece dovete pubblicare l’applicazione il comando da utilizzare è leggermente differente: npm run build.

npm run build

Questo genererà una cartella build nella root del progetto.

Build folder

Nella lista dei files se ne trovano due che hanno come nome package.json e package-lock.json. Il nome è molto simile e potrebbe trarre in inganno sul loro significato. Package.json definisce i package e le relative versioni da utilizzare nel progetto. Questo significa che lanciando il comando npm install il sistema in base alle versioni installate sulla macchina definirà quale combinazione di package utilizzare e creerà il file package-lock che a quel punto contiene esattamente tutti i package utilizzati sulla macchina.

[1] https://react.dev/

[2] https://create-react-app.dev/docs/folder-structure

[3] https://code.visualstudio.com/download

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

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