Anotaciones de datos de Code First

Nota

SOLO EF4.1 En adelante: las características, las API, etc. que se deban tratar en esta página se introdujeron en Entity Framework 4.1. Si usa una versión anterior, no se aplica parte o toda esta información.

El contenido de esta página se adapta a partir de un artículo escrito originalmente por Julia Lerman ( <http://thedatafarm.com> ).

Entity Framework Code First permite usar sus propias clases de dominio para representar el modelo en el que se basa EF para realizar funciones de consulta, seguimiento de cambios y actualización. Code First utiliza un patrón de programación denominado "convención sobre configuración". Code First asumirá que las clases siguen las convenciones de Entity Framework y, en ese caso, averiguará automáticamente cómo realizar su trabajo. Sin embargo, si las clases no siguen esas convenciones, tiene la capacidad de agregar configuraciones a las clases para proporcionar a EF la información necesaria.

Code First proporciona dos maneras de agregar estas configuraciones a las clases. Uno usa atributos simples denominados DataAnnotations y el segundo usa la API Fluent de Code First, que proporciona una manera de describir las configuraciones de forma imperativa, en el código.

Este artículo se centrará en el uso de DataAnnotations (en el espacio de nombres System.ComponentModel.DataAnnotations) para configurar las clases, resaltando las configuraciones más necesarias. DataAnnotations también se entiende mediante una serie de aplicaciones .NET, como ASP.NET MVC, que permite que estas aplicaciones aprovechen las mismas anotaciones para las validaciones del lado cliente.

El modelo

Mostraré Code First DataAnnotations con un par sencillo de clases: Blog y 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; }
    }

Tal y como están, las clases Blog y Post siguen cómodamente la convención code first y no requieren ajustes para habilitar la compatibilidad con EF. Sin embargo, también puede usar las anotaciones para proporcionar más información a EF sobre las clases y la base de datos a la que se asignan.

 

Clave

Entity Framework se basa en que cada entidad tiene un valor de clave que se usa para el seguimiento de entidades. Una convención de Code First son las propiedades de clave implícitas; Code First buscará una propiedad denominada "Id" o una combinación de nombre de clase e "Id", como "BlogId". Esta propiedad se asignará a una columna de clave principal de la base de datos.

Las clases Blog y Post siguen esta convención. ¿Qué ocurre si no lo han hecho? ¿Qué ocurre si blog usó el nombre PrimaryTrackingKey en su lugar, o incluso foo? Si el código no encuentra primero una propiedad que coincida con esta convención, se producirá una excepción debido al requisito de Entity Framework que debe tener una propiedad de clave. Puede usar la anotación de clave para especificar qué propiedad se va a usar como 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; }
    }

Si usa la característica de generación de base de datos de Code First, la tabla Blog tendrá una columna de clave principal denominada PrimaryTrackingKey, que también se define como Identidad de forma predeterminada.

Tabla de blog con clave principal

Claves compuestas

Entity Framework admite claves compuestas: claves principales que constan de más de una propiedad. Por ejemplo, podría tener una clase Passport cuya clave principal es una combinación de PassportNumber y 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; }
    }

Si se intenta usar la clase anterior en el modelo de EF, se produciría una excepción InvalidOperationException :

No se puede determinar la ordenación de la clave principal compuesta para el tipo "Passport". Use el método ColumnAttribute o HasKey para especificar un orden para las claves principales compuestas.

Para usar claves compuestas, Entity Framework requiere que defina un orden para las propiedades de clave. Para ello, use la anotación Columna para especificar un pedido.

Nota

El valor de orden es relativo (en lugar de basado en índices), por lo que se puede usar cualquier valor. Por ejemplo, 100 y 200 serían aceptables en lugar de 1 y 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; }
    }

Si tiene entidades con claves externas compuestas, debe especificar el mismo orden de columna que usó para las propiedades de clave principal correspondientes.

Solo el orden relativo dentro de las propiedades de clave externa debe ser el mismo, los valores exactos asignados a Order no necesitan coincidir. Por ejemplo, en la clase siguiente, se podrían usar 3 y 4 en lugar de 1 y 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; }
    }

Obligatorio

La Required anotación indica a EF que se requiere una propiedad determinada.

Agregar Requerido a la propiedad Title forzará a EF (y MVC) a asegurarse de que la propiedad tiene datos en ella.

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

Sin cambios de código ni marcado adicionales en la aplicación, una aplicación MVC realizará la validación del lado cliente, incluso generando dinámicamente un mensaje con los nombres de propiedad y anotación.

Error de creación de una página con título

El atributo Required también afectará a la base de datos generada al hacer que la propiedad asignada no acepta valores NULL. Observe que el campo Title ha cambiado a "not null".

Nota

En algunos casos, es posible que la columna de la base de datos no acepta valores NULL aunque se requiera la propiedad . Por ejemplo, cuando se usa una estrategia de herencia de TPH, los datos para varios tipos se almacenan en una sola tabla. Si un tipo derivado incluye una propiedad obligatoria, la columna no se puede convertir en que no acepta valores NULL, ya que no todos los tipos de la jerarquía tendrán esta propiedad.

 

Tabla de blogs

 

MaxLength y MinLength

Los MaxLength atributos y permiten especificar MinLength validaciones de propiedades adicionales, como hizo con Required .

Este es el valor de Bloname con requisitos de longitud. En el ejemplo también se muestra cómo combinar atributos.

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

La anotación MaxLength afectará a la base de datos estableciendo la longitud de la propiedad en 10.

Tabla de blogs en la que se muestra la longitud máxima en la columna DenombreDeDeSono

La anotación del lado cliente de MVC y la anotación del lado servidor de EF 4.1 respetarán esta validación y, de nuevo, crearán dinámicamente un mensaje de error: "El campo NombreDeAnchivo debe ser una cadena o un tipo de matriz con una longitud máxima de '10'". Ese mensaje es un poco largo. Muchas anotaciones permiten especificar un mensaje de error con el atributo ErrorMessage.

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

También puede especificar ErrorMessage en la anotación Required.

Creación de una página con un mensaje de error personalizado

 

NotMapped

La convención code first dicta que todas las propiedades que son de un tipo de datos admitido se representan en la base de datos. Pero esto no siempre es así en las aplicaciones. Por ejemplo, podría tener una propiedad en la clase Blog que crea un código basado en los campos Title y Bloname. Esa propiedad se puede crear dinámicamente y no es necesario almacenarla. Puede marcar cualquier propiedad que no se asigne a la base de datos con la anotación NotMapped, como esta propiedad BlogCode.

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

 

ComplexType

No es raro describir las entidades de dominio en un conjunto de clases y, a continuación, crear capas de esas clases para describir una entidad completa. Por ejemplo, puede agregar una clase denominada BlogDetails al modelo.

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

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

Observe que BlogDetails no tiene ningún tipo de propiedad de clave. En el diseño controlado por dominios, BlogDetails se conoce como un objeto de valor. Entity Framework hace referencia a objetos de valor como tipos complejos.  No se puede realizar un seguimiento de los tipos complejos por sí mismos.

Sin embargo, como propiedad de la clase , se realizará el Blog seguimiento como parte de un objeto BlogDetailsBlog . Para que el código reconozca primero esto, debe marcar la BlogDetails clase como ComplexType .

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

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

Ahora puede agregar una propiedad en la Blog clase para representar para ese BlogDetails blog.

        public BlogDetails BlogDetail { get; set; }

En la base de datos, la tabla contendrá todas las propiedades del blog, incluidas las Blog propiedades contenidas en su BlogDetail propiedad . De forma predeterminada, cada una de ellas va precedida del nombre del tipo complejo, "BlogDetail".

Tabla de blog con tipo complejo

ConcurrencyCheck

La anotación permite marcar una o varias propiedades que se usarán para la comprobación de simultaneidad en la base de datos cuando un usuario edite o ConcurrencyCheck elimine una entidad. Si ha estado trabajando con EF Designer, esto se alinea con el establecimiento de una propiedad ConcurrencyMode en Fixed .

Veamos cómo funciona ConcurrencyCheck agregándole a la BloggerName propiedad .

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

Cuando se llama a , debido a la anotación en el campo , se usará el valor original de esa SaveChangesConcurrencyCheck propiedad en la BloggerName actualización. El comando intentará buscar la fila correcta filtrando no solo por el valor de clave, sino también por el valor original de BloggerName .  Estas son las partes críticas del comando UPDATE enviadas a la base de datos, donde puede ver que el comando actualizará la fila que tiene un es 1 y un de PrimaryTrackingKey "Julia", que era el valor original cuando ese blog se recuperó de la base de BloggerName datos.

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

Si alguien ha cambiado el nombre del escritor para ese blog mientras tanto, se producirá un error en esta actualización y recibirá una excepción DbUpdateConcurrencyException que deberá controlar.

 

TimeStamp

Es más común usar los campos rowversion o timestamp para la comprobación de simultaneidad. Pero en lugar de usar la anotación , puede usar la anotación más específica siempre y cuando el ConcurrencyCheck tipo de la propiedad sea una TimeStamp matriz de bytes. En primer lugar, el código tratará las propiedades igual que las propiedades, pero también se asegurará de que el campo de base de datos que genera primero el código no acepta valores TimestampConcurrencyCheck NULL. Solo puede tener una propiedad timestamp en una clase determinada.

Agregar la siguiente propiedad a la clase Blog:

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

da como resultado que el código cree primero una columna timestamp que no acepta valores NULL en la tabla de base de datos.

Tabla de blogs con columna de marca de tiempo

 

Tabla y columna

Si deja que Code First la base de datos, puede que desee cambiar el nombre de las tablas y columnas que está creando. También puede usar Code First con una base de datos existente. Pero no siempre es el caso de que los nombres de las clases y propiedades del dominio coincidan con los nombres de las tablas y columnas de la base de datos.

Mi clase se denomina y, por convención, el código presupone primero que se asignará Blog a una tabla denominada Blogs . Si ese no es el caso, puede especificar el nombre de la tabla con el Table atributo . Aquí, por ejemplo, la anotación especifica que el nombre de la tabla es InternalBlogs.

    [Table("InternalBlogs")]
    public class Blog

La Column anotación es un elemento más adepto a la especificación de los atributos de una columna asignada. Puede estipular un nombre, un tipo de datos o incluso el orden en el que aparece una columna en la tabla. Este es un ejemplo del Column atributo .

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

No confunda el atributo de Column TypeName con DataType DataAnnotation. DataType es una anotación que se usa para la interfaz de usuario y Code First.

Esta es la tabla después de que se haya regenerado. El nombre de la tabla ha cambiado a InternalBlogs y la columna del tipo complejo es ahora BlogDescription . Dado que el nombre se especificó en la anotación, el código no usará primero la convención de iniciar el nombre de columna con el nombre del tipo complejo.

Se ha cambiado el nombre de la tabla y columna de blogs

 

DatabaseGenerated

Una de las características importantes de la base de datos es la capacidad de tener propiedades calculadas. Si va a asignar las clases Code First a tablas que contienen columnas calculadas, no Entity Framework intentar actualizar esas columnas. Pero quiere que EF devuelva esos valores de la base de datos después de insertar o actualizar los datos. Puede usar la DatabaseGenerated anotación para marcar esas propiedades en la clase junto con Computed la enumeración . Otras enumeraciones son None y Identity .

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

Puede usar la base de datos generada en columnas de bytes o de marca de tiempo cuando el código primero genera la base de datos; de lo contrario, solo debe usarlo al apuntar a bases de datos existentes porque el código primero no podrá determinar la fórmula para la columna calculada.

Ha leído anteriormente que, de forma predeterminada, una propiedad de clave que es un entero se convertirá en una clave de identidad en la base de datos. Sería lo mismo que establecer DatabaseGenerated en DatabaseGeneratedOption.Identity . Si no desea que sea una clave de identidad, puede establecer el valor en DatabaseGeneratedOption.None .

 

Índice

Nota

SOLO EF6.1 En adelante: el atributo se introdujo en Entity Framework 6.1. Si usa una versión anterior, no se aplica la información de esta sección.

Puede crear un índice en una o varias columnas mediante IndexAttribute. Agregar el atributo a una o varias propiedades hará que EF cree el índice correspondiente en la base de datos cuando cree la base de datos o scaffolde las llamadas a CreateIndex correspondientes si usa Migraciones de Code First.

Por ejemplo, el código siguiente dará lugar a la creación de un índice en la Rating columna de la tabla de la base de Posts datos.

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

De forma predeterminada, el índice se denominará IX_ nombre de propiedad > (IX_Rating en el ejemplo anterior). Sin embargo, también puede especificar un nombre para el índice. En el ejemplo siguiente se especifica que el índice debe denominarse PostRatingIndex .

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

De forma predeterminada, los índices no son únicos, pero puede usar el parámetro con nombre IsUnique para especificar que un índice debe ser único. En el ejemplo siguiente se presenta un índice único en el User nombre de inicio de sesión de un .

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

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

        public string DisplayName { get; set; }
    }

Multiple-Column índices

Los índices que abarcan varias columnas se especifican con el mismo nombre en varias anotaciones de índice para una tabla determinada. Al crear índices de varias columnas, debe especificar un orden para las columnas del índice. Por ejemplo, el código siguiente crea un índice de varias columnas en Rating y se llama BlogIdRating. BlogId es la primera columna del índice y Rating es la segunda.

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

 

Atributos de relación: InverseProperty y ForeignKey

Nota

En esta página se proporciona información sobre cómo configurar relaciones en el modelo Code First mediante anotaciones de datos. Para obtener información general sobre las relaciones en EF y cómo acceder a los datos y manipularlos mediante relaciones, vea Propiedades de navegación de relaciones.*

La convención code first se ocupará de las relaciones más comunes del modelo, pero hay algunos casos en los que necesita ayuda.

Al cambiar el nombre de la propiedad key de la Blog clase se creó un problema con su relación con Post

Al generar la base de datos, el código ve primero la propiedad en la clase Post y la reconoce, según la convención de que coincide con un nombre de clase más id. , como una clave externa para la BlogIdBlogIdBlog clase. Pero no hay ninguna BlogId propiedad en la clase de blog. La solución para esto es crear una propiedad de navegación en y usar Post DataAnnotation para ayudar primero al código a comprender cómo crear la relación entre las dos clases (mediante la propiedad ), así como cómo especificar restricciones en la base de ForeignKeyPost.BlogId datos.

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

La restricción de la base de datos muestra una relación entre InternalBlogs.PrimaryTrackingKey y Posts.BlogId

relación entre InternalBlogs.PrimaryTrackingKey y Posts.BlogId

se InverseProperty usa cuando tiene varias relaciones entre clases.

En la clase , es posible que desee realizar un seguimiento de quién escribió una entrada de blog y Post quién la editó. Estas son dos nuevas propiedades de navegación para la clase Post.

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

También deberá agregar en la clase a la que Person hacen referencia estas propiedades. La clase tiene propiedades de navegación de vuelta a , una para todas las publicaciones escritas por la persona y otra para todas las publicaciones actualizadas PersonPost por esa 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; }
    }

El código primero no puede hacer coincidir las propiedades de las dos clases por sí solo. La tabla de base de datos de debe tener una clave externa para la persona y otra para la persona, pero el código creará primero cuatro propiedades de clave PostsCreatedByUpdatedBy externa: Posts, CreatedBy, UpdatedByy UpdatedBy_Id.

Tabla De publicaciones con claves externas adicionales

Para corregir estos problemas, puede usar la InverseProperty anotación para especificar la alineación de las propiedades.

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

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

Dado que PostsWritten la propiedad de Person sabe que esto hace referencia al tipo , Post compilará la relación en Post.CreatedBy . De forma similar, PostsUpdated se conectará a Post.UpdatedBy . Y el código no creará primero las claves externas adicionales.

Tabla De publicaciones sin claves externas adicionales

 

Resumen

DataAnnotations no solo le permite describir la validación del lado cliente y del lado servidor en las clases de code first, sino que también le permiten mejorar e incluso corregir las suposiciones que el código realizará primero sobre las clases en función de sus convenciones. Con DataAnnotations no solo puede impulsar la generación de esquemas de base de datos, sino que también puede asignar las primeras clases de código a una base de datos ya existente.

Aunque son muy flexibles, tenga en cuenta que DataAnnotations solo proporciona los cambios de configuración más comunes que puede realizar en las clases de código primero. Para configurar las clases para algunos de los casos perimetrales, debe buscar en el mecanismo de configuración alternativo, Code First api de Fluent servidor .