Annotazioni dei dati per Code First

Nota

EF4.1 In seguito - Le funzionalità, le API e così via 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 basato su EF per eseguire query, rilevamento modifiche e funzioni di aggiornamento. Code First sfrutta un modello di programmazione denominato "convenzione sulla configurazione". Code First presuppone che le classi seguono le convenzioni di Entity Framework e, in questo caso, funzioneranno automaticamente come eseguire il processo. Tuttavia, se le classi non seguono queste convenzioni, è possibile aggiungere configurazioni alle classi per fornire alle 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 offre un modo per descrivere le configurazioni in modo imperativo, nel codice.

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

Il modello

Mostrerò 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; }
    }

Man mano che sono, le classi Blog e Post seguono in modo pratico la prima convenzione del codice e non richiedono modifiche per abilitare la compatibilità EF. Tuttavia, è anche possibile usare le annotazioni per fornire altre informazioni a EF sulle classi e sul database a cui vengono mappati.

 

Chiave

Entity Framework si basa su ogni entità con un valore chiave usato per il rilevamento delle entità. Una convenzione di Code First è proprietà chiave implicita; Code First cercherà una proprietà denominata "ID" o una combinazione di nome della 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 il blog ha usato il nome PrimaryTrackingKey o anche foo? Se il codice prima non trova una proprietà corrispondente a questa convenzione, genera 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 di code first, la tabella blog avrà una colonna chiave primaria denominata PrimaryTrackingKey, definita anche come Identity per impostazione predefinita.

Tabella di blog con chiave primaria

Chiavi composte

Entity Framework supporta chiavi composte: chiavi primarie costituite da più 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; }
    }

Il tentativo di usare la classe precedente nel modello EF comporta un InvalidOperationExceptionrisultato:

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 composte.

Per usare chiavi composte, Entity Framework richiede di definire un ordine per le proprietà delle chiavi. È possibile eseguire questa operazione usando l'annotazione Colonna per specificare un ordine.

Nota

Il valore dell'ordine è relativo (anziché basato su indice) in modo che tutti i valori possano essere usati. 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 composte, è necessario specificare lo stesso ordinamento di colonna usato per le proprietà 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. Ad esempio, nella classe seguente, è possibile usare 3 e 4 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; }
    }

Necessario

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

L'aggiunta di Obbligatorio alla proprietà Title forza ef (e MVC) per assicurarsi che la proprietà disponga dei dati in esso contenuti.

    [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 un messaggio in modo dinamico usando i nomi di proprietà e annotazione.

È necessario creare una pagina con titolo.

L'attributo Required influisce 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 non sia nullable anche se la proprietà è necessaria. Ad esempio, quando si usano dati di ereditarietà TPH per più tipi viene archiviato 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à.

 

Tabella Blog

 

MaxLength e MinLength

Gli MaxLength attributi e MinLength consentono di specificare le convalida delle proprietà aggiuntive, esattamente come si è 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 influisce sul database impostando la lunghezza della proprietà su 10.

Tabella dei blog che mostra la lunghezza massima nella colonna BloggerName

L'annotazione lato client MVC e l'annotazione lato server EF 4.1 consentirà di rispettare questa convalida, creando di nuovo dinamicamente un messaggio di errore: "Il campo BloggerName deve essere un tipo stringa o matrice con una lunghezza massima di '10'". Questo 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.

Creare una pagina con messaggio di errore personalizzato

 

NotMapped

La prima convenzione del codice determina che ogni proprietà di un tipo di dati supportato è rappresentata nel database. Ma questo non è sempre il caso nelle applicazioni. Ad esempio, potrebbe essere presente una proprietà nella classe Blog che crea un codice in base ai campi Title e BloggerName. Tale proprietà può essere creata dinamicamente e non deve essere archiviata. È possibile contrassegnare tutte le proprietà che non vengono mappate 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 livello 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 ha alcun tipo di proprietà chiave. Nella progettazione basata su dominio viene BlogDetails definito oggetto value. 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à monitorata come parte di un Blog oggetto. Per riconoscere prima di tutto il codice, è 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 classe per rappresentare l'oggetto BlogBlogDetails per tale 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, ogni oggetto è preceduto dal nome del tipo complesso "BlogDetail".

Tabella di blog con tipo complesso

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 EF Designer, questa opzione è allineata 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 nel 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 inviato al database, in cui è possibile visualizzare il comando aggiornerà la riga con un PrimaryTrackingKey valore 1 e una BloggerName di "Julie" che era il valore originale quando tale blog è stato recuperato dal database.

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

Se qualcuno ha modificato il nome del blogger per il 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. Tuttavia, anziché usare l'annotazione ConcurrencyCheck , è possibile usare l'annotazione più specifica TimeStamp purché il tipo della proprietà sia la matrice di byte. Timestamp Il codice considera prima le proprietà uguali ConcurrencyCheck alle proprietà, ma garantisce anche che il campo del database che il codice genera prima sia non 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.

Tabella Blog con colonna timestamp

 

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 . In questo caso, 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 stabilire 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 per primo non userà la convenzione di avviare il nome della colonna con il nome del tipo complesso.

Tabella e colonna blog rinominata

 

DatabaseGenerated

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. Ma si vuole che EF 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 in colonne di byte o timestamp quando il codice genera per primo il database. In caso contrario, è consigliabile usarlo solo quando punta ai database esistenti perché il codice 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. Equivale all'impostazione DatabaseGenerated di DatabaseGeneratedOption.Identitysu . Se non si vuole che sia una chiave di identità, è possibile impostare il valore su DatabaseGeneratedOption.None.

 

Indice

Nota

Solo EF6.1 E 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 quando crea il database o esegue 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 Multiple-Column

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 le annotazioni dei dati. Per informazioni generali sulle relazioni in Entity Framework e su come accedere e modificare i dati tramite relazioni, vedere Relazioni & Navigation Properties.*

La convenzione code first si occuperà delle relazioni più comuni nel modello, ma in alcuni casi richiede 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 esiste 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 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

relazione tra InternalBlogs.PrimaryTrackingKey e Posts.BlogId

l'oggetto InverseProperty viene usato quando si hanno più relazioni tra le classi.

Post Nella classe è possibile 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 dispone di proprietà di navigazione a Post, uno 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; }
    }

Code first non è in grado di trovare la 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 il codice creerà prima quattro proprietà di chiave esterna: Person_Id, Person_Id1, CreatedBy_Id e UpdatedBy_Id.

Tabella Post con chiavi esterne aggiuntive

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 , creerà la relazione con Post.CreatedBy. Analogamente, PostsUpdated verrà connesso a Post.UpdatedBy. E il codice per primo non creerà le chiavi esterne aggiuntive.

Tabella Post senza chiavi esterne aggiuntive

 

Riepilogo

DataAnnotations non solo consente di descrivere la convalida lato client e server nelle classi code first, ma consente anche di migliorare e correggere anche i presupposti che il codice creerà prima di tutto sulle classi in base alle 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.

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