Primeiras convenções de código personalizado
Observação
EF6 em diante apenas: os recursos, as APIs etc. discutidos nessa página foram introduzidos no Entity Framework 6. Se você estiver usando uma versão anterior, algumas ou todas as informações não se aplicarão.
Ao usar o Código Primeiro, seu modelo é calculado de suas classes usando um conjunto de convenções. As convenções padrão Code First determinar coisas como qual propriedade se torna a chave primária de uma entidade, o nome da tabela para a qual uma entidade é mapeada e qual precisão e escala uma coluna decimal tem por padrão.
Às vezes, essas convenções padrão não são ideais para seu modelo e você precisa contorná-las configurando muitas entidades individuais usando anotações de dados ou a API fluente. As Primeiras Convenções de Código Personalizado permitem que você defina suas próprias convenções que fornecem padrões de configuração para seu modelo. Neste passo a passo, exploraremos os diferentes tipos de convenções personalizadas e como criar cada uma delas.
Convenções baseadas em modelo
Esta página aborda a API DbModelBuilder para convenções personalizadas. Essa API deve ser suficiente para criar a maioria das convenções personalizadas. No entanto, também há a capacidade de criar convenções baseadas em modelo - convenções que manipulam o modelo final depois que ele é criado - para lidar com cenários avançados. Para obter mais informações, consulte as Convenções baseadas em modelo.
Nosso modelo
Vamos começar definindo um modelo simples que podemos usar com nossas convenções. Adicione as seguintes classes ao seu projeto.
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; }
}
Introdução a convenções personalizadas
Vamos escrever uma convenção que configura qualquer propriedade chamada Key para ser a chave primária para seu tipo de entidade.
As convenções são habilitadas no construtor de modelos, que pode ser acessado substituindo OnModelCreating no contexto. Atualize a classe ProductContext da seguinte maneira:
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());
}
}
Agora, qualquer propriedade em nosso modelo denominada Chave será configurada como a chave primária de qualquer entidade da qual ela faça parte.
Também poderíamos tornar nossas convenções mais específicas filtrando o tipo de propriedade que vamos configurar:
modelBuilder.Properties<int>()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
Isso configurará todas as propriedades chamadas Key como a chave primária de sua entidade, mas somente se elas forem um inteiro.
Um recurso interessante do método IsKey é que ele é aditivo. O que significa que, se você chamar IsKey em várias propriedades e todas elas se tornarem parte de uma chave composta. A única ressalva para isso é que, ao especificar várias propriedades para uma chave, você também deve especificar uma ordem para essas propriedades. Você pode fazer isso chamando o método HasColumnOrder, como abaixo:
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));
Esse código configurará os tipos em nosso modelo para ter uma chave composta que consiste na coluna de chave int e na coluna Nome da cadeia de caracteres. Se exibirmos o modelo no designer, ele será semelhante a este:
Outro exemplo de convenções de propriedade é configurar todas as propriedades DateTime em meu modelo para mapear para o tipo datetime2 no SQL Server em vez de datetime. Você pode fazer isso com o seguinte:
modelBuilder.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
Classes de convenção
Outra maneira de definir convenções é usar uma Classe de Convenção para encapsular sua convenção. Ao usar uma Classe de Convenção, você cria um tipo que herda da classe Convention no namespace System.Data.Entity.ModelConfiguration.Conventions.
Podemos criar uma Classe de Convenção com a convenção datetime2 que mostramos anteriormente fazendo o seguinte:
public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
}
}
Para dizer ao EF para usar essa convenção, adicione-a à coleção Conventions em OnModelCreating, que se você estiver acompanhando o passo a passo terá esta aparência:
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 você pode ver, adicionamos uma instância de nossa convenção à coleção de convenções. Herdar da Convenção fornece uma maneira conveniente de agrupar e compartilhar convenções entre equipes ou projetos. Você pode, por exemplo, ter uma biblioteca de classes com um conjunto comum de convenções que todos os projetos de suas organizações usam.
Atributos personalizados
Outro ótimo uso de convenções é permitir que novos atributos sejam usados ao configurar um modelo. Para ilustrar isso, vamos criar um atributo que podemos usar para marcar as propriedades de cadeia de caracteres como não Unicode.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NonUnicode : Attribute
{
}
Agora, vamos criar uma convenção para aplicar esse atributo ao nosso modelo:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
.Configure(c => c.IsUnicode(false));
Com essa convenção, podemos adicionar o atributo NonUnicode a qualquer uma de nossas propriedades de cadeia de caracteres, o que significa que a coluna no banco de dados será armazenada como varchar em vez de nvarchar.
Uma coisa a observar sobre essa convenção é que, se você colocar o atributo NonUnicode em qualquer outra coisa que não seja uma propriedade de cadeia de caracteres, ela gerará uma exceção. Ele faz isso porque você não pode configurar o IsUnicode em qualquer tipo que não seja uma cadeia de caracteres. Se isso acontecer, você poderá tornar sua convenção mais específica, para que ela filtre qualquer coisa que não seja uma cadeia de caracteres.
Embora a convenção acima funcione para definir atributos personalizados, há outra API que pode ser muito mais fácil de usar, especialmente quando você deseja usar propriedades da classe de atributo.
Para este exemplo, vamos atualizar nosso atributo e alterá-lo para um atributo IsUnicode, para que ele tenha esta aparência:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
internal class IsUnicode : Attribute
{
public bool Unicode { get; set; }
public IsUnicode(bool isUnicode)
{
Unicode = isUnicode;
}
}
Depois que tivermos isso, podemos definir um bool em nosso atributo para informar à convenção se uma propriedade deve ou não ser Unicode. Poderíamos fazer isso na convenção que já temos acessando o ClrProperty da classe de configuração como esta:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
.Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
Isso é bastante fácil, mas há uma maneira mais sucinta de conseguir isso usando o método Having da API de convenções. O método Having tem um parâmetro do tipo Func<PropertyInfo, T> que aceita o PropertyInfo da mesma forma que o método Where, mas espera-se que retorne um objeto. Se o objeto retornado for nulo, a propriedade não será configurada, o que significa que você pode filtrar as propriedades com ele exatamente como Onde, mas é diferente, pois ele também capturará o objeto retornado e o passará para o método Configure. Isso funciona da seguinte maneira:
modelBuilder.Properties()
.Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
.Configure((config, att) => config.IsUnicode(att.Unicode));
Atributos personalizados não são o único motivo para usar o método Having, é útil em qualquer lugar que você precise raciocinar sobre algo que você está filtrando ao configurar seus tipos ou propriedades.
Configurando tipos
Até agora, todas as nossas convenções foram para propriedades, mas há outra área da API de convenções para configurar os tipos em seu modelo. A experiência é semelhante às convenções que vimos até agora, mas as opções dentro da configuração estarão na entidade em vez do nível da propriedade.
Uma das coisas para as quais as convenções de nível de tipo podem ser realmente úteis é alterar a convenção de nomenclatura de tabela, seja para mapear para um esquema existente que difere do padrão EF ou para criar um novo banco de dados com uma convenção de nomenclatura diferente. Para fazer isso, primeiro precisamos de um método que possa aceitar o TypeInfo para um tipo em nosso modelo e retornar qual deve ser o nome da tabela para esse tipo:
private string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
Esse método usa um tipo e retorna uma cadeia de caracteres que usa maiúsculas e minúsculas com sublinhados em vez de CamelCase. Em nosso modelo, isso significa que a classe ProductCategory será mapeada para uma tabela chamada product_category em vez de ProductCategories.
Depois que tivermos esse método, podemos chamá-lo em uma convenção como esta:
modelBuilder.Types()
.Configure(c => c.ToTable(GetTableName(c.ClrType)));
Esta convenção configura todos os tipos em nosso modelo para mapear para o nome da tabela que é retornado do nosso método GetTableName. Essa convenção é equivalente a chamar o método ToTable para cada entidade no modelo usando a API fluente.
Uma coisa a observar sobre isso é que quando você chama ToTable EF usará a cadeia de caracteres que você fornece como o nome exato da tabela, sem qualquer pluralização que normalmente faria ao determinar nomes de tabela. É por isso que o nome da tabela de nossa convenção é product_category em vez de product_categories. Podemos resolver isso em nossa convenção fazendo uma chamada para o serviço de pluralização nós mesmos.
No código a seguir, usaremos o recurso Resolução de dependência adicionado no EF6 para recuperar o serviço de pluralização que o EF teria usado e pluralizar nosso nome de tabela.
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();
}
Observação
A versão genérica do GetService é um método de extensão no namespace System.Data.Entity.Infrastructure.DependencyResolution, você precisará adicionar uma instrução using ao seu contexto para poder usá-lo.
ToTable e Herança
Outro aspecto importante do ToTable é que, se você mapear explicitamente um tipo para uma determinada tabela, poderá alterar a estratégia de mapeamento que o EF usará. Se você chamar ToTable para cada tipo em uma hierarquia de herança, passando o nome do tipo como o nome da tabela como fizemos acima, você alterará a estratégia de mapeamento TPH (Tabela por Hierarquia) padrão para TPT (Tabela por Tipo). A melhor maneira de descrever isso é um exemplo concreto:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Manager : Employee
{
public string SectionManaged { get; set; }
}
Por padrão, tanto o funcionário quanto o gerente são mapeados para a mesma tabela (Funcionários) no banco de dados. A tabela conterá funcionários e gerentes com uma coluna discriminatória que informará que tipo de instância é armazenada em cada linha. Esse é o mapeamento de TPH, pois há uma única tabela para a hierarquia. No entanto, se você chamar ToTable em ambas as classes, cada tipo será mapeado para sua própria tabela, também conhecida como TPT, já que cada tipo tem sua própria tabela.
modelBuilder.Types()
.Configure(c=>c.ToTable(c.ClrType.Name));
O código acima será mapeado para uma estrutura de tabela semelhante à seguinte:
Você pode evitar isso e manter o mapeamento TPH padrão de algumas maneiras:
- Chame ToTable com o mesmo nome de tabela para cada tipo na hierarquia.
- Chame ToTable apenas na classe base da hierarquia, em nosso exemplo que seria funcionário.
Ordem de Execução
As convenções operam de maneira de última vitória, o mesmo que a API fluente. O que isso significa é que, se você escrever duas convenções que configuram a mesma opção da mesma propriedade, a última a ser executada ganhará. Por exemplo, no código abaixo do comprimento máximo de todas as cadeias de caracteres é definido como 500, mas configuramos todas as propriedades chamadas Nome no modelo para ter um comprimento máximo de 250.
modelBuilder.Properties<string>()
.Configure(c => c.HasMaxLength(500));
modelBuilder.Properties<string>()
.Where(x => x.Name == "Name")
.Configure(c => c.HasMaxLength(250));
Como a convenção para definir o comprimento máximo como 250 é depois daquela que define todas as cadeias de caracteres como 500, todas as propriedades chamadas Nome em nosso modelo terão um MaxLength de 250, enquanto qualquer outra cadeia de caracteres, como descrições, seria 500. Usar convenções dessa forma significa que você pode fornecer uma convenção geral para tipos ou propriedades em seu modelo e, em seguida, substituí-las por subconjuntos que sejam diferentes.
A API fluente e anotações de dados também podem ser usadas para substituir uma convenção em casos específicos. Em nosso exemplo acima, se tivéssemos usado a API fluente para definir o comprimento máximo de uma propriedade, poderíamos tê-la colocado antes ou depois da convenção, porque a API Fluent mais específica conquistará a Convenção de Configuração mais geral.
Convenções internas
Como as convenções personalizadas podem ser afetadas pelas convenções padrão do Code First, pode ser útil adicionar convenções para serem executadas antes ou depois de outra convenção. Para fazer isso, você pode usar os métodos AddBefore e AddAfter da coleção Conventions em seu DbContext derivado. O código a seguir adicionaria a classe de convenção que criamos anteriormente para que ela seja executada antes da convenção de descoberta de chave interna.
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
Isso será mais útil ao adicionar convenções que precisam ser executadas antes ou depois das convenções internas, uma lista das convenções internas pode ser encontrada aqui: System.Data.Entity.ModelConfiguration.Conventions Namespace.
Você também pode remover convenções que não deseja aplicar ao seu modelo. Para remover uma convenção, use o método Remove. Aqui está um exemplo de remoção do PluralizingTableNameConvention.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}