Site icon Ryadel

ASP.NET Entity Framework 6.1 - Elenco esaustivo delle Data Annotations

Abilitare e disabilitare il Lazy Loading con Entity Framework

A corredo dei numerosi articoli su EF6 scritti negli ultimi mesi, presentiamo una sintesi dell'ottimo articolo scritto da Julie Lerman e ripreso dal MSDN Data Developer Center, che ha il pregio di presentare in modo ordinato tutte le data annotation messe a disposizione da Entity Framework 6 per corredare le nostre classi in modo da orientare nel modo giusto la generazione della base dati in ottica code-first.

L'approccio code-first, per chi non lo sapesse già, è un metodo di sviluppo che consente di concentrarsi sul domain design, ovvero sulla creazione delle classi che rappresenteranno il nostro model, sfruttando la capacità di Entity Framework di creare automaticamente la struttura dati (il database, per intenderci) necessaria ad ospitarle.

In questa modalità di sviluppo le Data Annotation rivestono un ruolo fondamentale, in quanto consentono di orientare al meglio la generazione automatica del database definendo una serie di caratteristiche specifiche per ciascuna proprietà delle nostre model classes nel caso in cui le convenzioni normalmente adottate da EF non siano ottimali per i nostri scopi.

Il Model

Prendiamo a esempio il seguente modello:

Come possiamo vedere si tratta di due classi estremamente semplici, che possiedono già tutte le caratteristiche necessarie per essere tradotte in tabelle in ottica code-first secondo le convenzioni di Entity Framework. Vediamo come possiamo decorarle con le Data Annotations per fornire indicazioni più dettagliate su come strutturare le tabelle, le colonne e i campi secondo direttive personalizzate.

Key

Come ben sappiamo, è buona norma dotare ogni tabella di una chiave primaria che renda possibile identificare univocamente ciascun record contenuto. Nel caso di Entity Framework si tratta di una esigenza fondamentale, quindi il sistema adotta la convenzione di attribuire un valore di primary key alla colonna corrispondente a una proprietà chiamata Id o, se mancante, di una proprietà che combina il nome della classe e "Id" (UserId, PostId). Le nostre classi adottano un naming pienamente compatibile con questa convenzione, ma in alcuni casi potrebbe non essere così: ecco quindi che viene in soccorso la Data Annotation [Key], che non ha altro scopo se non quello di indicare a EF quale colonna utilizzare come primary key indipendentemente dal proprio nome. Nel caso in cui la proprietà Id della nostra classe User si chiamasse in modo diverso, ad esempio, potremmo ottenere un risultato analogo nel seguente modo:

E' importante notare che l'annotazione [Key] può essere utilizzata anche per creare chiavi primarie composite, formate cioè da più colonne. Nel caso in cui volessimo creare una primary key formata dall'Id e dal nome del nostro utente, potremmo fare nel seguente modo:

E' importante notare che in caso di chiavi composite è necessario specificare, oltre all'annotazione [Key], anche l'ordinamento delle colonne da indicizzare mediante l'annotazione [Column(Order=<n>)]: in caso contrario, infatti, al momento della generazione del database verrà restituito il seguente errore:

Unable to determine composite primary key ordering for type 'Passport'. Use the ColumnAttribute or the HasKey method to specify an order for composite primary keys.

ForeignKey

Annotazione molto simile alla precedente, consente di indicare a EF la presenza di eventuali chiavi esterne, relative cioè a primary key di classi collegate da una relazione: è il caso della nostra classe Comments, che presenta la proprietà AuthorId.

Una differenza sostanziale della [ForeignKey] è che richiede la definizione della classe relativa alla chiave primaria utilizzata nella relazione: nel nostro caso si tratta ovviamente della classe User. Anche nel caso delle Foreign Key è possibile specificare chiavi composite: la sintassi  è la medesima che abbiamo già visto per [Key].

Riprendendo l'esempio precedente, avremo quindi:

Required

Questa annotazione costringe EF a creare la base dati assicurandosi che la proprietà indicata sia non-nullable, ovvero creata con l'impossibilità di avere un valore nullo. Un buon punto per utilizzarla potrebbe essere a corredo del nome dei nostri utenti, nel seguente modo:

La definizione di questa annotazione provocherà anche, nel caso in cui il model venga utilizzato per rappresentare i dati di input di un form, la generazione automatica di una client-side validation volta ad assicurarsi che alla proprietà non venga assegnato un valore nullo. Inutile dire che l'assegnazione diretta di una model class a una vista andrebbe contro i principi del pattern MVC: lo stesso attributo può però essere utilizzato con successo da un model view (pattern MVVM), ottenendo lo stesso risultato.

MinLength e MaxLength

Come suggerisce il loro nome, le annotazioni [MinLength] e [MaxLength] consentono di definire il numero minimo e/o massimo di caratteri che deve avere una proprietà. Possono essere utilizzati separatamente oppure combinati, separandoli con una virgola. Ad esempio, il codice seguente assicurerà che il titolo di ciascun commento sia composto da un numero di caratteri minimo adeguato (10), senza però sconfinare al di là dei limiti imposti da una ipotetica interfaccia di visualizzazione (100).

ErrorMessage

Non si tratta di una annotazione bensì di un attributo che consente di personalizzare il messaggio di errore predefinito che EF visualizza quando annotation come [Required], [MaxLength], [MinLength] non vengono rispettate. Questo è un esempio di come può essere utilizzato:

E' opportuno sottolineare come, al posto di un testo literal, possa essere indicato anche un valore fortemente tipizzato - purché costante - come ad esempio una stringa proveniente da un file di risorse precompilato.

NotMapped

Questa annotazione serve a indicare le proprietà che non devono essere rappresentate all'interno del database creato da EF. Può essere il caso di proprietà dinamiche, magari che costruiscono il proprio valore sulla base di altre proprietà, o più semplicemente di proprietà accessorie che non abbiamo alcun interesse a memorizzare. Ad esempio, potremmo essere intenzionati a non memorizzare la data di creazione dei nostri commenti:

ComplexType

Questa annotazione va utilizzata a livello di classe e consente di definire una classe come un ComplexType, ovvero un sottoinsieme di proprietà relative a una model class vera e propria. Il modo migliore per spiegare questo concetto è ricorrere ad un esempio: supponiamo di voler corredare la nostra classe User di una serie di proprietà relative ad alcune caratteristiche dell'utente, come ad esempio dove vive: IndirizzoCittà e Telefono. Possiamo creare tre proprietà distinte oppure ricorrere al seguente ComplexType:

Come si può vedere, la seconda classe non contiene una chiave primaria e non corrisponderà quindi a una tabella: EF provvederà a creare, all'interno della tabella User, una serie di proprietà collegate al ComplexType in modo da rendere possibile dotare ciascun utente di quelle informazioni. Il ComplexType, in altre parole, non è che un modo per raggruppare in modo strutturato alcune proprietà specifiche - solitamente relative a un determinato contesto - di una model class.

ConcurrencyCheck

Questa annotazione consente di fare in modo che EF utilizzi una o più proprietà per effettuare un concurrency check in caso di operazioni di UPDATE o DELETE sulla entity in questione. Per spiegare questo concetto è opportuno ricorrere a un esempio. Ipotizziamo di contrassegnare in questo modo il nome della nostra entity User:

In base a questa annotazione il sistema, al momento di effettuare un UPDATE o DELETE, tenterà di localizzare la riga del Database basandosi sia sulla chiave primaria che sul valore originario della colonna FullName - quello che la riga aveva al momento di essere richiesta al DB. Nel caso in cui un altro operatore abbia effettuato una modifica al FullName nel lasso di tempo intercorso tra la richiesta originaria e l'UPDATE o DELETE in questione il comando fallirà, restituendo una eccezione di tipo DbUpdateConcurrencyException che sarà necessario gestire.

TimeStamp

Molto simile al [ConcurrencyCheck] menzionato sopra, questa annotazione effettua lo stesso controllo sulla proprietà in questione con l'aggiunta di assicurarsi che il campo Database generato dal code-first sia un non-nullable field, ovvero non accetti valori nulli. E' importante tenere presente che, all'interno di un Model, può essere definita solo una proprietà con questa annotazione.

Table

Questa annotazione, da assegnare a livello di classe, consente di specificare il nome della tabella relativa al Model che code-first utilizzerà all'interno del Database: questa funzionalità che torna spesso utile per risolvere problemi legati a omonimie rispetto a tabelle già presenti nel Db che impedirebbero la generazione della tabella secondo le convenzioni predefinite, che prevedono che quest'ultima abbia lo stesso nome del Model. Nell'esempio seguente, code-first assegnerà alla tabella relativa agli User il nome personalizzato MyUser:

Column

Similmente a quanto descritto per la precedente [Table], questa annotazione consente di specificare il nome di una o più colonne presenti nel Model. A differenza della precedente, assegnata a livello di classe, questa va obbligatoriamente assegnata a livello di proprietà. Nell'esempio seguente, code-first farà corrispondere alla proprietà FullName la colonna personalizzata MyFullName:

DatabaseGenerated

Nel caso in cui il Database contenga colonne con valore calcolato è possibile utilizzare questa annotazione per impedire Entity Framework provi a modificarne il valore, limitandosi a recuperarlo in read-only. I valori possibili sono Computed, None e Identity.

Nella maggior parte dei casi questa annotazione viene utilizzata  per colonne relative a tabelle già esistenti, in quanto code-first non sarà in grado di comprendere la logica di generazione del campo calcolato. L'utilizzo della DatabaseGenerationOption.Identity è scarsamente utilizzato in quanto, come spiegato sopra, nella maggior parte dei casi l'attributo [Key] e le convenzioni di EF consentono di definire automaticamente la chiave primaria della maggior parte delle tabelle.

Index

L'attributo [Index] (IndexAttribute) consente di creare un indice su una o più colonne. L'aggiunta dell'attributo a una o più proprietà farà sì che EF crei l'indice corrispondente nel database quando crea il database o generi le query CreateIndex corrispondenti se si utilizzano migrazioni Code First.

Ad esempio, il codice seguente comporterà la creazione di un indice nella colonna FullName della tabella Users nel database.

Ciascun indice verrà creato con un nome predefinito pari a IX_<nomeproprietà>: esiste comunque la possibilità di assegnare un nome specifico nel seguente modo:

La possibilità di assegnare un nome all'indice consente anche di creare indici che riguardino più di una colonna: è sufficiente assegnare il medesimo nome e assegnare un ordinamento alle colonne, come nell'esempio seguente:

E' importante sottolineare che gli indici vengono creati senza alcun controllo di univocità, prevedono dunque la possibilità che esistano due o più entry con lo stesso valore. Per abilitare il controllo di univocità occorre specificarlo in modo esplicito, come nell'esempio seguente:

InverseProperty

Questa annotazione, assegnabile unicamente a livello di proprietà, consente di indicare a EF come risolvere "all'indietro" alcune dipendenze specifiche nel caso in cui non sia possibile farlo automaticamente. Per comprendere meglio questo concetto, riprendiamo la classe Comment così come era stata modificata per spiegare il funzionamento dell'annotazione [ForeignKey]:

Ipotizziamo ora di voler creare un riferimento all'autore, dotando la classe Comment di una proprietà Author:

Come si può vedere, poiché non abbiamo utilizzato il nome UserId previsto dalla convenzione ma abbiamo optato per AuthorId, siamo costretti a definire manualmente la struttura a livello di ForeignKey. Cosa succede però se vogliamo dotare anche la classe User di un riferimento ai commenti scritti da quell'autore? E' qui che l'annotazione [InverseProperty] viene in nostro soccorso, consentendoci di indicarlo in modo esplicito:

In tal modo il framework sarà in grado di costruire la relazione e, se necessario, ricostruirla a ritroso, evitando la creazione di chiavi esterne aggiuntive.

Per una maggiore comprensione dell'utilizzo delle ForeignKey e delle relazioni consigliamo in ogni caso di fare riferimento a quest'ottimo articolo dalla documentazione ufficiale.

Conclusioni

In questa breve panoramica che illustra le principali Data Annotations messe a disposizione da Entity Framework abbiamo visto come queste siano uno strumento fondamentale per svolgere non soltanto processi di convalida client-side e server-side, ma anche e soprattutto di controllare, direzionare e correggere le molteplici assunzioni che EF inevitabilmente effettua al momento di generare il proprio Database (approccio code-first) ovvero interagire con un Database preesistente. Al tempo stesso, è importante tenere presente due concetti fondamentali:

  • Gli standard e le convenzioni di Entity Framework, se adottate, consentono di far risparmiare tempo prezioso e riducono la necessità di riempire il proprio codice di Data Annotations: per questo motivo è altamente consigliabile, soprattutto in ottica code-first, fare del proprio meglio per conformarsi ad esse, limitando l'approccio descritto in questo articolo ai casi in cui questo non è possibile.
  • Nonostante siano estremamente potenti, le Data Annotations sono uno strumento per molti aspetti limitato e che consente di effettuare modifiche relativamente superficiali: nel caso in cui si abbia bisogno di istruzioni più complesse è consigliabile utilizzare un approccio noto come Code-First's Fluent API, illustrato dettagliatamente in questo articolo dalla guida MSDN ufficiale e che sarà presto oggetto di un articolo apposito su questo blog.

 

Exit mobile version