ENTITY FRAMEWORK CORE

MAPPARE LE CLASSI DI ENTITA’

NET COREL’approccio Database First non è pienamente supportato da Entity Framework e questo perché il Tool che abbiamo usato è distruttivo. Supponiamo di voler aggiungere una colonna ad una nostra tabella, però vogliamo che la medesima colonna ce la ritroviamo nella corrispettiva classe di entità, se eseguiamo di nuovo il Tool usato in precedenza, tutte le classi di entità verrebbero soprascritte facendoci perdere ogni eventuale modifica apportata. Quindi ATTENZIONE se creiamo una nuova colonna nel database dobbiamo essere noi a mappare manualmente la corrispettiva proprietà nella classe di entità.

APPROCCIO CODE FIRST

L’alternativa è l’approccio Code First, prima ci creiamo le classi di entità e le rispettive relazioni e lasciamo a Entity Framework Core creare il database con il meccanismo delle Migrations. La questione delle Migrations la vedremo più avanti per il momento facciamo noi il mapping tra il mondo concettuale e quello relazionale. Finché la proprietà ha lo stesso nome della colonna del database possiamo evitare il mapping. La classe di entità fa parte del codice applicativo e perciò dovrebbe contenere anche logica applicativa a differenza dei ViewModel.

Classi di entità

L’immagine riporta il codice della classe di entità Course.

Course

Vediamo il mapping con l’interfaccia fluente di Entity Framework Core.

Mapping

MAPPARE LE RELAZIONI E I TIPI COMPLESSI

Vediamo come viene mappata la classe Money e le relazioni tra Courses e Lessons.

Owned Types

Le informazioni monetarie occorre che siano vicine, per questo le abbiamo incapsulate in una classe Money che ha due proprietà, la valuta che è un Enum e l’importo. Vediamo il mapping.

Mapping owned types

Ora andiamo a vedere come si mappano le relazioni.

Relazioni

HasMany(…) indica che un corso può avere molte lezioni. Con WithOne(…) ci portiamo dalla parte delle lezioni. Per una lezione esiste un solo corso. L’ultima proprietà indica la chiave esterna che nel nostro caso sarà CourseId.

Chiave esterna

ESTRARRE L’ELENCO DEI CORSI DA UN SERVIZIO APPLICATIVO

Il MyCourseDbContext che abbiamo creato fa parte del servizio infrastrutturale. Vediamo come usarlo da una classe del servizio applicativo che chiameremo EfCoreCourseService, che ti ricordo deve implementare l’interfaccia ICourseService.

Servizi

Vediamo il metodo di estrazione dei corsi. E’ un metodo asincrono perché ti ricordo che Entity Framework Core deve collegarsi al database e inviare query SQL nel momento in cui facciamo richiesta dei dati dal database.

Estrazione dei corsi

ESTRARRE LE ENTITA’ CORRELATE DEL CORSO

Vediamo il codice. L’istruzione AsNoTracking() è un’ottimizzazione della query. Per estrarre le lezioni dobbiamo usare l’extension Method Include a cui va fornita una lambda.

Dettaglio e lezioni del corso

COMPRENDERE IQUERYABLE<T> E LA DEFERRED EXECUTION

Abbiamo visto che Entity Framework Core ci consente di lavorare su un modello concettuale, costruito su una serie di oggetti chiamati entità e interrogabili con query Linq. Non dobbiamo mai commettere l’errore di abbandonarci a tutta questa comodità dimenticando che sotto comunque c’è un database relazionale. Ritornando al metodo di estrazione dei corsi, la query SQL viene inviata al database quando manifestiamo l’intenzione di leggere i dati, questo per motivi di efficienza.

Deferred execution
IQuerable

USARE TECNICHE PER MIGLIORARE LE PERFORMANCE

Vediamo l’istruzione AsNoTracking().

AsNoTracking

Vediamo l’altro accorgimento. Abbiamo già parlato di Connection Pool e con Entity Framework Core il funzionamento è analogo.

ContextPool

RIEPILOGO DELLA SEZIONE

USARE UN ORM

Un ORM (Object Relational Mapper) è una tecnologia che consente l’accesso ai dati situati in database relazionale. I dati possono essere recuperati con query fortemente tipizzate, anziché con query SQL che sono inclini a farci commettere errori di digitazione.

L’ORM si occupa di “tradurre” in SQL le nostre query fortemente tipizzate. Inoltre, legge i risultati trovati dal database e ce li restituisce in forma di oggetti (o grafi di oggetti) grazie a una fase che si chiama materializzazione. Un ORM è quindi uno strato di astrazione che tende ad ovviare alle rigidità del mondo relazionale e ci permette di sfruttare tutta l’espressività di un linguaggio come il C#. D’altra parte, un ORM ha un costo prestazionale proprio perché deve eseguire della logica di mapping per “adattare” le peculiarità del mondo relazionale a quelle del mondo a oggetti.

L’ORM che abbiamo valutato si chiama Entity Framework Core ed è stato costruito da Microsoft su ADO.NET, che usa a basso livello per interagire con il database. Ci permette sia di inviare query fortemente tipizzate con LINQ e, all’occorrenza, anche query SQL.

LAMBDA EXPRESSION

Scrivere una lambda expression è un modo molto conciso di definire una funzione anonima che accetta parametri e restituisce un valore proprio qualsiasi altro metodo C#. Ecco un esempio:

  1. //Definisco una lambda expression che riceve due parametri string e restituisce un valore string
  2. //L’operatore lambda, cioè il simbolo =>, separa i parametri dal corpo
  3. (string firstName, string lastName) => $”{firstName} {lastName}”;

Questa scrittura è equivalente alla definizione del seguente metodo (tranne il nome, che nella lambda è assente).

  1. private string NomeMetodo(string firstName, string lastName)
  2. {
  3.        return $”{firstName} {lastName}”;
  4. }

Come dice il nome stesso, una lambda expression è un’espressione e perciò può essere assegnata a una variabile oppure passata come argomento nell’invocare un altro metodo. Ecco un esempio:

  1. //Assegno la lambda a una variabile di nome getFullName
  2. //Qui i tipi dei parametri non sono necessari: il compilatore li inferisce dal tipo della variabile
  3. Func<string, string, string> getFullName = (firstName, lastName) => $”{firstName} {lastName}”;
  4. //E poi la passo come argomento nell’invocare un metodo chiamato SortPeople
  5. SortPeople(getFullName);

A che scopo passare una lambda expression come argomento? Beh, in questo modo possiamo fornire una nostra logica personalizzata a un altro metodo che invece si occuperà di fare il lavoro “pesante”.

Ad esempio, se il metodo si occupa di ordinare un elenco, noi con la lambda expression ci limitiamo ad indicargli il criterio secondo il quale vogliamo che l’elenco sia ordinato (ad esempio: per nome oppure prima per nome e poi per cognome, ecc…).

Le lambda expression possono essere assegnate a variabili di vari tipi:

  • Func<T1, T2, …, TResult> Il tipo Func è usato quando la lambda expression deve restituire un valore. L’ultimo argomento di tipo (TResult) indica di che tipo dovrà essere il valore restituito. Tutti i precedenti argomenti di tipo (T1, T2, …) indicano il numero e il tipo di argomenti che la lambda expression deve accettare;
  • Action<T1, T2, …> Il tipo Action è usato quando la lambda expression non deve restituire nulla (equivalente a un metodo void). Gli argomenti di tipo (T1, T2, …) indicano il numero e il tipo di parametri che la lambda deve accettare.

COMPORRE QUERY LINQ

LINQ è una tecnologia che serve a interrogare elenchi di elementi, cioè oggetti che implementano l’interfaccia IEnumerable. Per “interrogare” si intende filtrare, ordinare, proiettare e aggregare l’elenco secondo le nostre logiche. A tale scopo fornisce una moltitudine di extension method, definiti nel namespace System.Linq, che usiamo per comporre delle query LINQ.

Ad esempio, l’extension method Where si occupa di filtrare un elenco di elementi cioè di produrre un nuovo elenco contenente un numero variabile di elementi, da 0 a tutti, in base alla nostra logica che gli passeremo con una lambda expression. Ecco un esempio:

  1. //Dato un array di 6 numeri interi
  2. var numbers = new int[] { 1, 2, 3, 4, 5, 6 }; 
  3. //Vogliamo ottenere l’elenco dei soli numeri pari
  4. //La lambda che forniamo viene chiamata per ciascuno dei numeri in elenco
  5. //E restituirà true se il resto della divisione per 2 è 0
  6. var evenNumbers = numbers.Where(number => number % 2 == 0);
  7. //E li stampiamo con un foreach
  8. foreach (var evenNumber in evenNumbers)
  9. {
  10. WriteLine(evenNumber);
  11. }

LINQ possiede numerosi extension method che vedremo nel corso del tempo. Eccone solo alcuni:

  • Where produce un nuovo elenco contenente solo gli elementi conformi al nostro criterio di filtro;
  • Select proietta gli elementi, cioè produce un nuovo elenco i cuoi elementi sono di altro tipo;
  • Sum, Count e Average producono un valore scalare sommando, conteggiando o calcolando la media degli elementi in elenco.

INIZIARE CON ENTITY FRAMEWORK CORE

Entity Framework Core, proprio come ADO.NET, è in grado di funzionare con varie tecnologie database grazie ai provider realizzati da vari produttori. Per iniziare, dobbiamo quindi installare il pacchetto NuGet idoneo, che ci procurerà il provider per Sqlite. Nel nostro caso, eseguiamo il comando:

  1. dotnet add package Microsoft.EntityFrameworkCore.Sqlite

L’elenco completo delle tecnologie database supportate e dei relativi provider si trova a questo indirizzo:
https://docs.microsoft.com/it-it/ef/core/providers/

CREARE IL MODELLO CONCETTUALE

Possiamo seguire due approcci:

  • Se seguiamo l’approccio database-first vuol dire che abbiamo già creato il nostro database e perciò possiamo generare automaticamente le classi del modello concettuale. Microsoft ha messo a disposizione il comando dotnet ef dbcontext scaffold in grado di fare il reverse engineering del database, cioè esaminare le tabelle esistenti e, per ciascuna, generare la rispettiva classe;
  • Se invece seguiamo l’approccio code-first allora dobbiamo scrivere noi a mano le classi del modello concettuale. In seguito faremo generare lo schema del database a Entity Framework Core grazie alle migration, che vedremo più avanti.

È importante notare che l’approccio database-first non è propriamente supportato da Entity Framework Core. Infatti, il tool di reverse engineering non è in grado di rilevare le modifiche che apportiamo di volta in volta alla struttura del database e perciò non è in grado di aggiornare le classi nel modello concettuale.

Oltre alle classi del modello concettuale, nel nostro progetto ci sarà una classe che deriva da DbContext in cui inseriremo il mapping tra modello a oggetti e modello relazionale.

MAPPARE IL MODELLO CONCETTUALE AL MODELLO RELAZIONALE

Entity Framework Core fa da tramite tra modello a oggetti e modello relazionale. Affinché sia in grado di adempiere a questa responsabilità dobbiamo istruirlo su come ogni classe del modello concettuale rimappa sulla rispettiva tabella (o tabelle) nel database. Il mapping lo indichiamo nel metodo OnModelCreating del DbContext.

Entity Framework Core dispone di un’interfaccia fluente che ci permette di mappare:

  • Classi di entità: sono le classi come Course e Lesson, che hanno una propria identità (cioè una chiave primaria);
  • Owned types: sono classi come Money, prive di una propria identità e create al solo scopo di tenere coesi alcuni valori (come la valuta e il valore di un importo monetario).
  • Relazioni: sono i vincoli che legano tra loro due classi di entità. Ad esempio tra Course e Lesson esiste un rapporto di uno-a-molti (per ogni corso esistono più lezioni).

Grazie al mapping, possiamo modellare le classi come preferiamo e sfruttare così tutte le potenzialità del linguaggio C# (ereditarietà, proprietà complesse, ecc…). In questo modo non siamo più costretti a seguire una struttura dati rigida come quella del mondo relazionale.

Inoltre, nel metodo OnConfiguring del DbContext dobbiamo ovviamente indicare quale tecnologia database vogliamo usare e qual è la stringa di connessione. Per finire, creiamo delle proprietà nel DbContext che espongono i DbSet, ovvero gli oggetti a cui rivolgeremo le nostre query LINQ

REGISTRARE IL DBCONTEXT PER LA DEPENDENCY INJECTION

Entity Framework Core è ben integrato con il meccanismo di dependency injection di ASP.NET Core. I nostri servizi applicativi possono quindi avere una dipendenza dal DbContext, purché sia stato registrato dal metodo ConfigureServices della classe Startup.

In alternativa possiamo usare il metodo AddDbContextPool, che permetterà di avere migliori performance perché le istanze del nostro DbContext saranno create in maniera proattiva, in maniera simile al funzionamento del connection pool di ADO.NET.

È importante notare che la query SQL viene inviata al database il più tardi possibile, cioè in corrispondenza della riga di codice in cui andiamo a leggere i risultati. Questa peculiarità si chiama deferred execution.

Altre situazioni in cui Entity Framework Core invia la query SQL sono:

  • Quando cicliamo la query LINQ con un foreach;
  • Quando usiamo altri extension method di aggregazione come Count, Average, Sum (e loro varianti asincrone).

Per ottimizzare le prestazioni, ricordiamoci di:

  • Usare l’extension method AsNoTracking se dobbiamo solo leggere i risultati. Così evitiamo che Entity Framework Core si metta a tracciare le modifiche alle entità, che in questo caso sarebbe superfluo;
  • Fare il mapping tra entità e viewmodel come un’unica espressione all’interno dell’extension method Select (come nell’esempio), altrimenti nella query SQL potrebbero essere coinvolti troppi campi, anche quelli che non ci servono.

LINK AL CODICE SU GITHUB

GITHUB

Scaricare il codice della sezione11 o clonare il repository GITHUB per avere a disposizione tutte le sezioni nel tuo editor preferito.