Annotazioni dei dati per Code First

Nota

EF4.1 Solo versioni successive: le funzionalità, le API e così via descritte in questa pagina sono state introdotte in Entity Framework 4.1. Se si usa una versione precedente, alcune o tutte queste informazioni non si applicano.

Il contenuto di questa pagina è adattato da un articolo originariamente scritto da Julie Lerman (<http://thedatafarm.com>).

Entity Framework Code First consente di usare le proprie classi di dominio per rappresentare il modello su cui EF si basa per eseguire query, rilevamento modifiche e funzioni di aggiornamento. Code First sfrutta un modello di programmazione denominato "convenzione sulla configurazione". Code First presupporrà che le classi seguano le convenzioni di Entity Framework e, in tal caso, funzioneranno automaticamente come eseguire il processo. Tuttavia, se le classi non seguono tali convenzioni, è possibile aggiungere configurazioni alle classi per fornire a Entity Framework le informazioni necessarie.

Code First offre due modi per aggiungere queste configurazioni alle classi. Uno usa attributi semplici denominati DataAnnotations e il secondo usa l'API Fluent di Code First, che consente di descrivere le configurazioni in modo imperativo, nel codice.

Questo articolo si concentrerà sull'uso di DataAnnotations (nello spazio dei nomi System.ComponentModel.DataAnnotations) per configurare le classi, evidenziando le configurazioni più comuni necessarie. Le dataAnnotations sono comprese anche da una serie di applicazioni .NET, ad esempio ASP.NET MVC, che consente a queste applicazioni di sfruttare le stesse annotazioni per le convalide lato client.

Il modello

Dimostrare Code First DataAnnotations con una semplice coppia di classi: Blog e Post.

    public class Blog
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public DateTime DateCreated { get; set; }
        public string Content { get; set; }
        public int BlogId { get; set; }
        public ICollection<Comment> Comments { get; set; }
    }

In quanto sono, le classi Blog e Post seguono comodamente la convenzione code first e non richiedono modifiche per abilitare la compatibilità di EF. Tuttavia, è anche possibile usare le annotazioni per fornire altre informazioni a Entity Framework sulle classi e sul database a cui eseguono il mapping.

 

Key

Entity Framework si basa su ogni entità con un valore di chiave usato per il rilevamento delle entità. Una convenzione di Code First è costituita da proprietà chiave implicite; Code First cercherà una proprietà denominata "Id" o una combinazione di nome di classe e "Id", ad esempio "BlogId". Questa proprietà verrà mappata a una colonna chiave primaria nel database.

Le classi Blog e Post seguono entrambe questa convenzione. E se non lo avessero fatto? Cosa accade se blog usava invece il nome PrimaryTrackingKey o persino foo? Se il codice non trova prima una proprietà che corrisponde a questa convenzione, genererà un'eccezione a causa del requisito di Entity Framework che è necessario disporre di una proprietà chiave. È possibile usare l'annotazione chiave per specificare quale proprietà deve essere usata come EntityKey.

    public class Blog
    {
        [Key]
        public int PrimaryTrackingKey { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

Se si usa la funzionalità di generazione del database code first, la tabella blog avrà una colonna chiave primaria denominata PrimaryTrackingKey, definita anche come Identity per impostazione predefinita.

Blog table with primary key

Chiavi composte

Entity Framework supporta chiavi composite: chiavi primarie costituite da più di una proprietà. Ad esempio, è possibile avere una classe Passport la cui chiave primaria è una combinazione di PassportNumber e IssuingCountry.

    public class Passport
    {
        [Key]
        public int PassportNumber { get; set; }
        [Key]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

Se si tenta di usare la classe precedente nel modello di Entity Framework, si ottiene un risultato InvalidOperationException:

Impossibile determinare l'ordinamento della chiave primaria composita per il tipo 'Passport'. Usare ColumnAttribute o il metodo HasKey per specificare un ordine per le chiavi primarie composite.

Per usare chiavi composite, Entity Framework richiede di definire un ordine per le proprietà della chiave. A tale scopo, è possibile usare l'annotazione Column per specificare un ordine.

Nota

Il valore dell'ordine è relativo (anziché basato su indice) in modo che sia possibile usare qualsiasi valore. Ad esempio, 100 e 200 sarebbero accettabili al posto di 1 e 2.

    public class Passport
    {
        [Key]
        [Column(Order=1)]
        public int PassportNumber { get; set; }
        [Key]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

Se si dispone di entità con chiavi esterne composite, è necessario specificare lo stesso ordinamento di colonna usato per le proprietà della chiave primaria corrispondenti.

Solo l'ordinamento relativo all'interno delle proprietà della chiave esterna deve essere lo stesso, i valori esatti assegnati a Order non devono corrispondere. Nella classe seguente, ad esempio, 3 e 4 possono essere usati al posto di 1 e 2.

    public class PassportStamp
    {
        [Key]
        public int StampId { get; set; }
        public DateTime Stamped { get; set; }
        public string StampingCountry { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 1)]
        public int PassportNumber { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }

        public Passport Passport { get; set; }
    }

Richiesto

L'annotazione Required indica a EF che è necessaria una particolare proprietà.

L'aggiunta di Required alla proprietà Title forza EF (e MVC) a garantire che la proprietà contenga dati.

    [Required]
    public string Title { get; set; }

Senza modifiche di codice o markup aggiuntive nell'applicazione, un'applicazione MVC eseguirà la convalida lato client, anche creando dinamicamente un messaggio usando i nomi di proprietà e annotazione.

Create page with Title is required error

L'attributo Required influirà anche sul database generato rendendo la proprietà mappata non nullable. Si noti che il campo Titolo è stato modificato in "not null".

Nota

In alcuni casi potrebbe non essere possibile che la colonna nel database sia non nullable anche se la proprietà è obbligatoria. Ad esempio, quando si usano dati di strategia di ereditarietà TPH per più tipi vengono archiviati in una singola tabella. Se un tipo derivato include una proprietà obbligatoria, la colonna non può essere resa non nullable perché non tutti i tipi nella gerarchia avranno questa proprietà.

 

Blogs table

 

MaxLength e MinLength

Gli MaxLength attributi e MinLength consentono di specificare convalide di proprietà aggiuntive, come è stato fatto con Required.

Ecco il BloggerName con requisiti di lunghezza. L'esempio illustra anche come combinare gli attributi.

    [MaxLength(10),MinLength(5)]
    public string BloggerName { get; set; }

L'annotazione MaxLength influirà sul database impostando la lunghezza della proprietà su 10.

Blogs table showing max length on BloggerName column

L'annotazione lato client MVC e l'annotazione lato server EF 4.1 rispeveranno questa convalida, creando di nuovo in modo dinamico un messaggio di errore: "Il campo BloggerName deve essere un tipo stringa o matrice con una lunghezza massima di '10'". Quel messaggio è un po' lungo. Molte annotazioni consentono di specificare un messaggio di errore con l'attributo ErrorMessage.

    [MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

È anche possibile specificare ErrorMessage nell'annotazione Obbligatoria.

Create page with custom error message

 

NotMapped

La convenzione code first determina che ogni proprietà di un tipo di dati supportato è rappresentata nel database. Ma questo non è sempre il caso nelle applicazioni. Ad esempio, potresti avere una proprietà nella classe Blog che crea un codice basato sui campi Title e BloggerName. Tale proprietà può essere creata in modo dinamico e non deve essere archiviata. È possibile contrassegnare tutte le proprietà che non eseguono il mapping al database con l'annotazione NotMapped, ad esempio questa proprietà BlogCode.

    [NotMapped]
    public string BlogCode
    {
        get
        {
            return Title.Substring(0, 1) + ":" + BloggerName.Substring(0, 1);
        }
    }

 

ComplexType

Non è raro descrivere le entità di dominio in un set di classi e quindi eseguire il layer di tali classi per descrivere un'entità completa. Ad esempio, è possibile aggiungere una classe denominata BlogDetails al modello.

    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

Si noti che BlogDetails non dispone di alcun tipo di proprietà chiave. Nella progettazione basata su dominio viene BlogDetails definito oggetto valore. Entity Framework fa riferimento a oggetti valore come tipi complessi.  I tipi complessi non possono essere rilevati autonomamente.

Tuttavia, come proprietà nella Blog classe , BlogDetails verrà tenuta traccia come parte di un Blog oggetto . Affinché il codice riconosca prima di tutto questo, è necessario contrassegnare la BlogDetails classe come .ComplexType

    [ComplexType]
    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

È ora possibile aggiungere una proprietà nella Blog classe per rappresentare l'oggetto BlogDetails per il blog.

        public BlogDetails BlogDetail { get; set; }

Nel database la Blog tabella conterrà tutte le proprietà del blog, incluse le proprietà contenute nella relativa BlogDetail proprietà. Per impostazione predefinita, ognuno di essi è preceduto dal nome del tipo complesso "BlogDetail".

Blog table with complex type

ConcurrencyCheck

L'annotazione ConcurrencyCheck consente di contrassegnare una o più proprietà da usare per il controllo della concorrenza nel database quando un utente modifica o elimina un'entità. Se si lavora con Entity Framework Designer, questo è allineato all'impostazione di ConcurrencyMode una proprietà su Fixed.

Vediamo come ConcurrencyCheck funziona aggiungendolo alla BloggerName proprietà .

    [ConcurrencyCheck, MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

Quando SaveChanges viene chiamato, a causa dell'annotazione ConcurrencyCheck sul BloggerName campo, il valore originale di tale proprietà verrà usato nell'aggiornamento. Il comando tenterà di individuare la riga corretta filtrando non solo sul valore della chiave, ma anche sul valore originale di BloggerName.  Ecco le parti critiche del comando UPDATE inviate al database, in cui è possibile visualizzare il comando aggiornerà la riga con un PrimaryTrackingKey valore 1 e una BloggerName di "Julie" che rappresentava il valore originale quando il blog è stato recuperato dal database.

    where (([PrimaryTrackingKey] = @4) and ([BloggerName] = @5))
    @4=1,@5=N'Julie'

Se qualcuno ha cambiato il nome del blogger per quel blog nel frattempo, questo aggiornamento avrà esito negativo e si otterrà un DbUpdateConcurrencyException che sarà necessario gestire.

 

TimeStamp

È più comune usare i campi rowversion o timestamp per il controllo della concorrenza. Invece di usare l'annotazione, è possibile usare l'annotazione ConcurrencyCheck più specifica TimeStamp , purché il tipo della proprietà sia una matrice di byte. Il codice prima tratterà Timestamp le proprietà uguali alle ConcurrencyCheck proprietà, ma garantirà anche che il campo del database generato per primo dal codice non sia nullable. È possibile avere una sola proprietà timestamp in una determinata classe.

Aggiunta della proprietà seguente alla classe Blog:

    [Timestamp]
    public Byte[] TimeStamp { get; set; }

genera prima la creazione di una colonna timestamp non nullable nella tabella di database.

Blogs table with time stamp column

 

Tabella e colonna

Se si consente a Code First di creare il database, è possibile modificare il nome delle tabelle e delle colonne che sta creando. È anche possibile usare Code First con un database esistente. Tuttavia, non è sempre il caso in cui i nomi delle classi e delle proprietà nel dominio corrispondano ai nomi delle tabelle e delle colonne nel database.

La classe è denominata Blog e, per convenzione, il codice presuppone che venga eseguito il mapping a una tabella denominata Blogs. In caso contrario, è possibile specificare il nome della tabella con l'attributo Table . Qui, ad esempio, l'annotazione specifica che il nome della tabella è InternalBlogs.

    [Table("InternalBlogs")]
    public class Blog

L'annotazione Column è più adept per specificare gli attributi di una colonna mappata. È possibile specificare un nome, un tipo di dati o anche l'ordine in cui viene visualizzata una colonna nella tabella. Di seguito è riportato un esempio dell'attributo Column .

    [Column("BlogDescription", TypeName="ntext")]
    public String Description {get;set;}

Non confondere l'attributo Column TypeName con DataType DataAnnotation. DataType è un'annotazione usata per l'interfaccia utente e viene ignorata da Code First.

Ecco la tabella dopo la rigenerazione. Il nome della tabella è stato modificato in InternalBlogs e Description la colonna dal tipo complesso è ora BlogDescription. Poiché il nome è stato specificato nell'annotazione, il codice prima non userà la convenzione di avviare il nome della colonna con il nome del tipo complesso.

Blogs table and column renamed

 

DatabaseGenerato

Un'importante funzionalità del database è la possibilità di avere proprietà calcolate. Se si esegue il mapping delle classi Code First alle tabelle che contengono colonne calcolate, non si vuole che Entity Framework tenti di aggiornare tali colonne. Tuttavia, si vuole che Entity Framework restituisca tali valori dal database dopo l'inserimento o l'aggiornamento dei dati. È possibile usare l'annotazione DatabaseGenerated per contrassegnare tali proprietà nella classe insieme all'enumerazione Computed . Altre enumerazioni sono None e Identity.

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime DateCreated { get; set; }

È possibile usare il database generato su colonne di byte o timestamp quando il codice genera il database. In caso contrario, è consigliabile usarlo solo quando punta ai database esistenti perché il codice per primo non sarà in grado di determinare la formula per la colonna calcolata.

Per impostazione predefinita, una proprietà chiave che è un numero intero diventerà una chiave identity nel database. Sarebbe uguale all'impostazione DatabaseGenerated di su DatabaseGeneratedOption.Identity. Se non si vuole che sia una chiave di identità, è possibile impostare il valore su DatabaseGeneratedOption.None.

 

Indice

Nota

EF6.1 Solo versioni successive: l'attributo Index è stato introdotto in Entity Framework 6.1. Se si usa una versione precedente, le informazioni contenute in questa sezione non si applicano.

È possibile creare un indice in una o più colonne usando IndexAttribute. L'aggiunta dell'attributo a una o più proprietà causerà la creazione dell'indice corrispondente nel database durante la creazione del database o lo scaffolding delle chiamate CreateIndex corrispondenti se si usa Migrazioni Code First.

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

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index]
        public int Rating { get; set; }
        public int BlogId { get; set; }
    }

Per impostazione predefinita, l'indice verrà denominato IX_<nome> della proprietà (IX_Rating nell'esempio precedente). È anche possibile specificare un nome per l'indice. Nell'esempio seguente viene specificato che l'indice deve essere denominato PostRatingIndex.

    [Index("PostRatingIndex")]
    public int Rating { get; set; }

Per impostazione predefinita, gli indici non sono univoci, ma è possibile usare il IsUnique parametro denominato per specificare che un indice deve essere univoco. Nell'esempio seguente viene introdotto un indice univoco in base al nome di accesso di un Useroggetto .

    public class User
    {
        public int UserId { get; set; }

        [Index(IsUnique = true)]
        [StringLength(200)]
        public string Username { get; set; }

        public string DisplayName { get; set; }
    }

Indici a più colonne

Gli indici che si estendono su più colonne vengono specificati usando lo stesso nome in più annotazioni di indice per una determinata tabella. Quando si creano indici a più colonne, è necessario specificare un ordine per le colonne nell'indice. Ad esempio, il codice seguente crea un indice a più colonne in Rating e BlogId denominato IX_BlogIdAndRating. BlogId è la prima colonna nell'indice ed Rating è la seconda.

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index("IX_BlogIdAndRating", 2)]
        public int Rating { get; set; }
        [Index("IX_BlogIdAndRating", 1)]
        public int BlogId { get; set; }
    }

 

Attributi di relazione: InverseProperty e ForeignKey

Nota

Questa pagina fornisce informazioni sulla configurazione delle relazioni nel modello Code First usando annotazioni dati. Per informazioni generali sulle relazioni in Entity Framework e su come accedere e modificare i dati tramite relazioni, vedere Relazioni e proprietà di navigazione.*

La convenzione code first si occuperà delle relazioni più comuni nel modello, ma in alcuni casi è necessario assistenza.

La modifica del nome della proprietà chiave nella Blog classe ha creato un problema con la relativa relazione con Post

Quando si genera il database, il codice vede innanzitutto la BlogId proprietà nella classe Post e la riconosce, in base alla convenzione che corrisponde a un nome di classe più ID, come chiave esterna alla Blog classe . Ma non c'è alcuna BlogId proprietà nella classe blog. La soluzione consiste nel creare una proprietà di navigazione in Post e usare DataAnnotation ForeignKey per consentire al codice di comprendere innanzitutto come compilare la relazione tra le due classi (usando la Post.BlogId proprietà ) e come specificare i vincoli nel database.

    public class Post
    {
            public int Id { get; set; }
            public string Title { get; set; }
            public DateTime DateCreated { get; set; }
            public string Content { get; set; }
            public int BlogId { get; set; }
            [ForeignKey("BlogId")]
            public Blog Blog { get; set; }
            public ICollection<Comment> Comments { get; set; }
    }

Il vincolo nel database mostra una relazione tra InternalBlogs.PrimaryTrackingKey e Posts.BlogId

relationship between InternalBlogs.PrimaryTrackingKey and Posts.BlogId

Viene InverseProperty usato quando si hanno più relazioni tra le classi.

Post Nella classe può essere utile tenere traccia di chi ha scritto un post di blog e di chi l'ha modificata. Ecco due nuove proprietà di navigazione per la classe Post.

    public Person CreatedBy { get; set; }
    public Person UpdatedBy { get; set; }

Sarà anche necessario aggiungere nella Person classe a cui fanno riferimento queste proprietà. La Person classe ha proprietà di spostamento a Post, una per tutti i post scritti dalla persona e uno per tutti i post aggiornati da tale persona.

    public class Person
    {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<Post> PostsWritten { get; set; }
            public List<Post> PostsUpdated { get; set; }
    }

Il codice first non è in grado di trovare una corrispondenza con le proprietà nelle due classi autonomamente. La tabella di database per Posts deve avere una chiave esterna per la CreatedBy persona e una per la UpdatedBy persona, ma code first creerà quattro proprietà di chiave esterna: Person_Id, Person_Id1, CreatedBy_Id e UpdatedBy_Id.

Posts table with extra foreign keys

Per risolvere questi problemi, è possibile usare l'annotazione InverseProperty per specificare l'allineamento delle proprietà.

    [InverseProperty("CreatedBy")]
    public List<Post> PostsWritten { get; set; }

    [InverseProperty("UpdatedBy")]
    public List<Post> PostsUpdated { get; set; }

Poiché la PostsWritten proprietà in Person sa che questo fa riferimento al Post tipo , compilerà la relazione con Post.CreatedBy. Analogamente, PostsUpdated verrà connesso a Post.UpdatedBy. E code first non creerà le chiavi esterne aggiuntive.

Posts table without extra foreign keys

 

Riepilogo

DataAnnotations non solo consente di descrivere la convalida lato client e server nelle classi code first, ma consente anche di migliorare e persino correggere i presupposti che il codice renderà prima sulle classi in base alle relative convenzioni. Con DataAnnotations è possibile non solo guidare la generazione dello schema del database, ma è anche possibile eseguire il mapping delle classi code first a un database preesistente.

Sebbene siano molto flessibili, tenere presente che DataAnnotations fornisce solo le modifiche di configurazione più comuni che è possibile apportare alle classi code first. Per configurare le classi per alcuni casi perimetrali, è necessario esaminare il meccanismo di configurazione alternativo, l'API Fluent di Code First.