Nota
L'accés a aquesta pàgina requereix autorització. Podeu provar d'iniciar la sessió o de canviar els directoris.
L'accés a aquesta pàgina requereix autorització. Podeu provar de canviar els directoris.
Nota:
Solo ef6 y versiones posteriores : las características, las API, etc. que se describen en esta página se introdujeron en Entity Framework 6. Si usa una versión anterior, no se aplica parte o toda la información.
Cuando se usa Code First, el modelo se calcula a partir de las clases mediante un conjunto de convenciones. Las convenciones predeterminadas de Code First determinan cosas como qué propiedad se convierte en la clave principal de una entidad, el nombre de la tabla a la que se asigna una entidad y a qué precisión y escala una columna decimal tiene de forma predeterminada.
A veces, estas convenciones predeterminadas no son ideales para el modelo y tiene que solucionarlas configurando muchas entidades individuales mediante anotaciones de datos o la API fluent. Las convenciones personalizadas de Code First permiten definir sus propias convenciones que proporcionan valores predeterminados de configuración para el modelo. En este tutorial, exploraremos los distintos tipos de convenciones personalizadas y cómo crear cada uno de ellos.
Convenciones Basadas en Modelos
En esta página se describe la API DbModelBuilder para convenciones personalizadas. Esta API debe ser suficiente para crear la mayoría de las convenciones personalizadas. Sin embargo, también existe la capacidad de crear convenciones basadas en modelos ( convenciones que manipulan el modelo final una vez creado) para controlar escenarios avanzados. Para obtener más información, consulte Convenciones Basadas en Modelos.
Nuestro modelo
Comencemos definiendo un modelo sencillo que podemos usar con nuestras convenciones. Agregue las siguientes clases al proyecto.
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
}
public class Product
{
public int Key { get; set; }
public string Name { get; set; }
public decimal? Price { get; set; }
public DateTime? ReleaseDate { get; set; }
public ProductCategory Category { get; set; }
}
public class ProductCategory
{
public int Key { get; set; }
public string Name { get; set; }
public List<Product> Products { get; set; }
}
Presentación de convenciones personalizadas
Vamos a escribir una convención que configure cualquier propiedad denominada Key para que sea la clave principal para su tipo de entidad.
Las convenciones están habilitadas en el generador de modelos, a las que se puede acceder invalidando OnModelCreating en el contexto. Actualice la clase ProductContext de la siguiente manera:
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
}
}
Ahora, cualquier propiedad del modelo denominada Key se configurará como la clave principal de cualquier entidad de la que forma parte.
También podríamos hacer que nuestras convenciones sean más específicas filtrando por el tipo de propiedad que vamos a configurar:
modelBuilder.Properties<int>()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
Esto configurará todas las propiedades llamadas Key para que sean la clave principal de su entidad, pero solo si son un entero.
Una característica interesante del método IsKey es que es aditivo. Esto significa que si llama a IsKey en varias propiedades, todas ellas se convertirán en parte de una clave compuesta. La única advertencia para esto es que, al especificar varias propiedades para una clave, también debe especificar un orden para esas propiedades. Para ello, llame al método HasColumnOrder como se indica a continuación:
modelBuilder.Properties<int>()
.Where(x => x.Name == "Key")
.Configure(x => x.IsKey().HasColumnOrder(1));
modelBuilder.Properties()
.Where(x => x.Name == "Name")
.Configure(x => x.IsKey().HasColumnOrder(2));
Este código configurará los tipos de nuestro modelo para que tengan una clave compuesta formada por la columna int Key y la columna Nombre de cadena. Si vemos el modelo en el diseñador, tendría este aspecto:
Otro ejemplo de convenciones de propiedad es configurar todas las propiedades DateTime de mi modelo para mapear al tipo datetime2 en SQL Server en lugar de datetime. Puede lograrlo con lo siguiente:
modelBuilder.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
Clases de convención
Otra manera de definir convenciones es usar una clase de convención para encapsular la convención. Cuando se usa una clase de convención, se crea un tipo que hereda de la clase Convention en el espacio de nombres System.Data.Entity.ModelConfiguration.Conventions.
Podemos crear una clase de convención con la convención datetime2 que mostramos anteriormente haciendo lo siguiente:
public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
}
}
Para indicar a EF que use esta convención, agréguela a la colección Conventions en OnModelCreating, que si ha seguido junto con el tutorial tendrá este aspecto:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<int>()
.Where(p => p.Name.EndsWith("Key"))
.Configure(p => p.IsKey());
modelBuilder.Conventions.Add(new DateTime2Convention());
}
Como puede ver, agregamos una instancia de nuestra convención a la colección de convenciones. Heredar de convención proporciona una forma cómoda de agrupar y compartir convenciones entre equipos o proyectos. Por ejemplo, podría tener una biblioteca de clases con un conjunto común de convenciones que usan todos los proyectos de las organizaciones.
Atributos personalizados
Otro gran uso de convenciones es permitir que se usen nuevos atributos al configurar un modelo. Para ilustrar esto, vamos a crear un atributo que se puede usar para marcar las propiedades String como no Unicode.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NonUnicode : Attribute
{
}
Ahora, vamos a crear una convención para aplicar este atributo a nuestro modelo:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
.Configure(c => c.IsUnicode(false));
Con esta convención podemos agregar el atributo NonUnicode a cualquiera de nuestras propiedades de cadena, lo que significa que la columna de la base de datos se almacenará como varchar en lugar de nvarchar.
Una cosa que hay que tener en cuenta sobre esta convención es que si coloca el atributo NonUnicode en algo distinto de una propiedad de cadena, se producirá una excepción. Esto se debe a que no se puede configurar IsUnicode en ningún tipo distinto de una cadena. Si esto sucede, puede hacer que la convención sea más específica, de modo que filtre todo lo que no sea una cadena.
Aunque la convención anterior funciona para definir atributos personalizados, hay otra API que puede ser mucho más fácil de usar, especialmente cuando desea usar propiedades de la clase de atributo.
En este ejemplo vamos a actualizar el atributo y cambiarlo a un atributo IsUnicode, por lo que tiene el siguiente aspecto:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
internal class IsUnicode : Attribute
{
public bool Unicode { get; set; }
public IsUnicode(bool isUnicode)
{
Unicode = isUnicode;
}
}
Una vez que tengamos esto, podemos establecer una bool en nuestro atributo para indicar a la convención si una propiedad debe ser Unicode o no. Podríamos lograrlo según la convención que ya utilizamos accediendo a ClrProperty de la clase de configuración de esta manera:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
.Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
Esto es lo suficientemente fácil, pero hay una manera más sucinta de lograr esto mediante el método Having de la API de convenciones. El método Having tiene un parámetro de tipo Func<PropertyInfo, T> que acepta PropertyInfo igual que el método Where, pero se espera que devuelva un objeto. Si el objeto devuelto es NULL, la propiedad no se configurará, lo que significa que puede filtrar las propiedades con él igual que Where, pero es diferente en que también capturará el objeto devuelto y lo pasará al método Configure. Esto funciona de la siguiente manera:
modelBuilder.Properties()
.Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
.Configure((config, att) => config.IsUnicode(att.Unicode));
Los atributos personalizados no son el único motivo para usar el método Having, es útil en cualquier situación en la que necesite razonar sobre alguna condición de filtro al configurar sus tipos o propiedades.
Configuración de tipos
Hasta ahora, todas nuestras convenciones han sido para las propiedades, pero hay otra área de la API de convenciones para configurar los tipos en el modelo. La experiencia es similar a las convenciones que hemos visto hasta ahora, pero las opciones dentro de la configuración estarán en la entidad en lugar de en el nivel de propiedad.
Una de las cosas para las que las convenciones del nivel de tipo pueden ser realmente útiles es cambiar la convención de nomenclatura de tablas, ya sea para asignarla a un esquema existente que difiera del valor predeterminado del Entity Framework o para crear una nueva base de datos con una convención de nomenclatura diferente. Para ello, primero necesitamos un método que pueda aceptar TypeInfo para un tipo en nuestro modelo y devolver el nombre de tabla para ese tipo:
private string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
Este método toma un tipo y devuelve una cadena que usa minúsculas con caracteres de subrayado en lugar de CamelCase. En nuestro modelo, esto significa que la clase ProductCategory se asignará a una tabla denominada product_category en lugar de ProductCategories.
Una vez que tengamos ese método, podemos llamarlo en una convención como esta:
modelBuilder.Types()
.Configure(c => c.ToTable(GetTableName(c.ClrType)));
Esta convención configura cada tipo de nuestro modelo para asignarlo al nombre de la tabla que se devuelve desde nuestro método GetTableName. Esta convención es equivalente a llamar al método ToTable para cada entidad del modelo mediante fluent API.
Una cosa que hay que tener en cuenta es que cuando se llama a ToTable EF se toma la cadena que se proporciona como el nombre exacto de la tabla, sin ninguna de las pluralizaciones que normalmente haría al determinar los nombres de tabla. Este es el motivo por el que el nombre de la tabla de nuestra convención es product_category en lugar de product_categories. Podemos resolverlo en nuestra convención haciendo una llamada al servicio de pluralización nosotros mismos.
En el código siguiente usaremos la característica De resolución de dependencias agregada en EF6 para recuperar el servicio de pluralización que EF habría usado y pluralizar el nombre de la tabla.
private string GetTableName(Type type)
{
var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();
var result = pluralizationService.Pluralize(type.Name);
result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
Nota:
La versión genérica de GetService es un método de extensión en el espacio de nombres System.Data.Entity.Infrastructure.DependencyResolution; necesitará agregar una instrucción using en su contexto para utilizarlo.
ToTable y herencia
Otro aspecto importante de ToTable es que, si asigna explícitamente un tipo a una tabla dada, puede modificar la estrategia de mapeo que utilizará Entity Framework. Si invoca ToTable para cada tipo en una jerarquía de herencia y pasa el nombre del tipo como el nombre de la tabla, como se hizo anteriormente, cambiará la estrategia de asignación predeterminada de Table-Per-Hierarchy (TPH) a Table-Per-Type (TPT). La mejor manera de describir esto es un ejemplo concreto:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Manager : Employee
{
public string SectionManaged { get; set; }
}
De forma predeterminada, tanto los empleados como el administrador se asignan a la misma tabla (Empleados) de la base de datos. La tabla contendrá empleados y administradores con una columna discriminante que le indicará qué tipo de instancia se almacena en cada fila. Esta es la asignación de TPH, puesto que hay una única tabla para la jerarquía. Sin embargo, si llama a ToTable en ambas clases, cada tipo se asignará a su propia tabla, también conocida como TPT, ya que cada tipo tiene su propia tabla.
modelBuilder.Types()
.Configure(c=>c.ToTable(c.ClrType.Name));
El código anterior se asignará a una estructura de tabla similar a la siguiente:
Puede evitarlo y mantener la asignación de TPH predeterminada de dos maneras:
- Llame a ToTable con el mismo nombre de tabla para cada tipo de la jerarquía.
- Utilice "ToTable" solo en la clase base de la jerarquía, en nuestro ejemplo que sería el empleado.
Orden de ejecución
Las convenciones funcionan de última manera, igual que la API fluent. Lo que significa es que si escribe dos convenciones que configuran la misma opción de la misma propiedad, la última que se va a ejecutar gana. Por ejemplo, en el código debajo de la longitud máxima de todas las cadenas se establece en 500, pero luego se configuran todas las propiedades denominadas Name en el modelo para tener una longitud máxima de 250.
modelBuilder.Properties<string>()
.Configure(c => c.HasMaxLength(500));
modelBuilder.Properties<string>()
.Where(x => x.Name == "Name")
.Configure(c => c.HasMaxLength(250));
Dado que la convención para establecer la longitud máxima en 250 es posterior a la que establece todas las cadenas en 500, todas las propiedades denominadas Name en nuestro modelo tendrán un Valor MaxLength de 250 mientras que cualquier otra cadena, como descripciones, sería 500. Utilizar convenciones de esta manera significa que puede proporcionar una convención general para los tipos o propiedades en su modelo y luego sobrescribirlas para subconjuntos específicos.
La API fluent y las anotaciones de datos también se pueden usar para invalidar una convención en casos específicos. En nuestro ejemplo anterior, si hubieramos usado fluent API para establecer la longitud máxima de una propiedad, podríamos haberla puesto antes o después de la convención, ya que la API de Fluent más específica ganará sobre la convención de configuración más general.
Convenciones integradas
Dado que las convenciones personalizadas podrían verse afectadas por las convenciones predeterminadas de Code First, puede resultar útil agregar convenciones para ejecutarse antes o después de otra convención. Para ello, puede usar los métodos AddBefore y AddAfter de la colección Conventions en el dbContext derivado. El código siguiente agregaría la clase de convención que creamos anteriormente para que se ejecute antes de la convención de detección de claves integrada.
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
Esto será más útil al agregar convenciones que necesitan ejecutarse antes o después de las convenciones predeterminadas, se puede encontrar una lista de las convenciones predeterminadas aquí: Espacio de nombres System.Data.Entity.ModelConfiguration.Conventions.
También puede quitar convenciones que no quiera aplicar al modelo. Para quitar una convención, use el método Remove. Este es un ejemplo de cómo quitar PluralizingTableNameConvention.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}