Configuração em massa do modelo

Quando um aspecto precisa ser configurado da mesma forma entre vários tipos de entidade, as técnicas a seguir permitem reduzir a duplicação de código e consolidar a lógica.

Consulte o projeto de exemplo completo que contém os Snippets de código apresentados abaixo.

Configuração em massa em OnModelCreating

Cada objeto de construtor retornado ModelBuilder expõe uma propriedade Model ou Metadata que fornece um acesso de baixo nível aos objetos que compõem o modelo. Em particular, há métodos que permitem iterar sobre objetos específicos no modelo e aplicar a configuração comum a eles.

No exemplo a seguir, o modelo contém um tipo de valor personalizado Currency:

public readonly struct Currency
{
    public Currency(decimal amount)
        => Amount = amount;

    public decimal Amount { get; }

    public override string ToString()
        => $"${Amount}";
}

As propriedades desse tipo não são descobertas por padrão, pois o provedor EF atual não sabe como mapeá-lo para um tipo de banco de dados. Esse trecho de OnModelCreating adiciona todas as propriedades do tipo Currency e configura um conversor de valores para um tipo compatível - decimal:

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    foreach (var propertyInfo in entityType.ClrType.GetProperties())
    {
        if (propertyInfo.PropertyType == typeof(Currency))
        {
            entityType.AddProperty(propertyInfo)
                .SetValueConverter(typeof(CurrencyConverter));
        }
    }
}
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
    public CurrencyConverter()
        : base(
            v => v.Amount,
            v => new Currency(v))
    {
    }
}

Desvantagens da API de Metadados

  • Ao contrário da API fluente, todas as modificações no modelo precisam ser feitas explicitamente. Por exemplo, se algumas das propriedades Currency foram configuradas como navegações por uma convenção, você precisará primeiro remover a navegação que faz referência à propriedade CLR antes de adicionar uma propriedade de tipo de entidade para ela. O #9117 melhorará isso.
  • As convenções são executadas após cada alteração. Se você remover uma navegação descoberta por uma convenção, a convenção será executada novamente e poderá adicioná-la de volta. Para evitar que isso aconteça, você precisará atrasar as convenções até que a propriedade seja adicionada chamando DelayConventions() e descartando posteriormente o objeto retornado ou marcar a propriedade CLR como ignorada usando AddIgnored.
  • Os tipos de entidade podem ser adicionados depois que essa iteração acontecer e a configuração não será aplicada a eles. Isso geralmente pode ser evitado colocando esse código no final do OnModelCreating, mas se você tiver dois conjuntos interdependentes de configurações, talvez não haja uma ordem que permita que elas sejam aplicadas de forma consistente.

Configuração de pré-convenção

O EF Core permite que a configuração de mapeamento seja especificada uma vez para um determinado tipo CLR; essa configuração é então aplicada a todas as propriedades desse tipo no modelo conforme elas são descobertas. Isso é chamado de "configuração de modelo de pré-convenção", pois configura aspectos do modelo antes que as convenções de criação de modelo possam ser executadas. Essa configuração é aplicada substituindo ConfigureConventions no tipo derivado de DbContext.

Este exemplo mostra como configurar todas as propriedades do tipo Currency para ter um conversor de valor:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<Currency>()
        .HaveConversion<CurrencyConverter>();
}

E este exemplo mostra como configurar algumas facetas em todas as propriedades do tipo string:

configurationBuilder
    .Properties<string>()
    .AreUnicode(false)
    .HaveMaxLength(1024);

Observação

O tipo especificado em uma chamada de ConfigureConventions pode ser um tipo base, uma interface ou uma definição de tipo genérico. Todas as configurações correspondentes serão aplicadas na ordem do menos específico:

  1. Interface
  2. Tipo base
  3. Definição de tipo genérico
  4. Tipo de valor não nulo
  5. Tipo exato

Importante

A configuração de pré-convenção é equivalente à configuração explícita que é aplicada assim que um objeto correspondente é adicionado ao modelo. Ele substituirá todas as convenções e anotações de dados. Por exemplo, com a configuração acima, todas as propriedades de chave estrangeira de cadeia de caracteres serão criadas como não unicode com MaxLength de 1024, mesmo que isso não corresponda à chave principal.

Ignorando tipos

A configuração de pré-convenção também permite ignorar um tipo e impedir que ele seja descoberto por convenções como um tipo de entidade ou como uma propriedade em um tipo de entidade:

configurationBuilder
    .IgnoreAny(typeof(IList<>));

Mapeamento de tipo padrão

Geralmente, o EF é capaz de traduzir consultas com constantes de um tipo que não tem suporte pelo provedor, desde que você tenha especificado um conversor de valor para uma propriedade desse tipo. No entanto, em consultas que não envolvem nenhuma propriedade desse tipo, não há como o EF encontrar o conversor de valor correto. Nesse caso, é possível chamar DefaultTypeMapping para adicionar ou substituir um mapeamento de tipo de provedor:

configurationBuilder
    .DefaultTypeMapping<Currency>()
    .HasConversion<CurrencyConverter>();

Limitações da configuração de pré-convenção

  • Muitos aspectos não podem ser configurados com essa abordagem. O #6787 expandirá isso para mais tipos.
  • Atualmente, a configuração só é determinada pelo tipo CLR. O #20418 permitiria predicados personalizados.
  • Essa configuração é executada antes de um modelo ser criado. Se houver conflitos que surgirem ao aplicá-lo, o rastreamento de pilha de exceção não conterá o método ConfigureConventions, portanto, pode ser mais difícil encontrar a causa.

Convenções

Observação

As convenções de criação de modelo personalizado foram introduzidas no EF Core 7.0.

As convenções de criação de modelo do EF Core são classes que contêm lógica que é disparada com base em alterações feitas no modelo conforme ele está sendo criado. Isso mantém o modelo atualizado à medida que a configuração explícita é feita, os atributos de mapeamento são aplicados e outras convenções são executadas. Para participar disso, cada convenção implementa uma ou mais interfaces que determinam quando o método correspondente será disparado. Por exemplo, uma convenção que implementa IEntityTypeAddedConvention será acionada sempre que um novo tipo de entidade for adicionado ao modelo. Da mesma forma, uma convenção que implementa IForeignKeyAddedConvention e IKeyAddedConvention será acionada sempre que uma chave ou uma chave estrangeira for adicionada ao modelo.

As convenções de construção de modelos são uma maneira poderosa de controlar a configuração do modelo, mas podem ser complexas e difíceis de acertar. Em muitos casos, a configuração do modelo de pré-convenção pode ser usada para especificar facilmente a configuração comum para propriedades e tipos.

Adicionando uma nova convenção

Exemplo: restringir o comprimento das propriedades discriminatórias

A estratégia de mapeamento de herança tabela por hierarquia requer uma coluna discriminatória para especificar qual tipo é representado em uma determinada linha. Por padrão, o EF usa uma coluna de cadeia de caracteres não associada para o discriminador, o que garante que ela funcione por qualquer comprimento discriminatório. No entanto, restringir o comprimento máximo das cadeias de caracteres do discriminador pode tornar o armazenamento e as consultas mais eficientes. Vamos criar uma nova convenção que faça isso.

As convenções de criação do modelo EF Core são acionadas com base nas alterações feitas no modelo à medida que ele está sendo criado. Isso mantém o modelo atualizado à medida que a configuração explícita é feita, os atributos de mapeamento são aplicados e outras convenções são executadas. Para participar disso, cada convenção implementa uma ou mais interfaces que determinam quando a convenção será acionada. Por exemplo, uma convenção que implementa IEntityTypeAddedConvention será acionada sempre que um novo tipo de entidade for adicionado ao modelo. Da mesma forma, uma convenção que implementa IForeignKeyAddedConvention e IKeyAddedConvention será acionada sempre que uma chave ou uma chave estrangeira for adicionada ao modelo.

Saber quais interfaces implementar pode ser complicado, já que a configuração feita no modelo em um ponto pode ser alterada ou removida em um ponto posterior. Por exemplo, uma chave pode ser criada por convenção, mas posteriormente substituída quando uma chave diferente é configurada explicitamente.

Vamos tornar isso um pouco mais concreto, fazendo uma primeira tentativa de implementar a convenção do comprimento discriminador:

public class DiscriminatorLengthConvention1 : IEntityTypeBaseTypeChangedConvention
{
    public void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        var discriminatorProperty = entityTypeBuilder.Metadata.FindDiscriminatorProperty();
        if (discriminatorProperty != null
            && discriminatorProperty.ClrType == typeof(string))
        {
            discriminatorProperty.Builder.HasMaxLength(24);
        }
    }
}

Essa convenção implementa IEntityTypeBaseTypeChangedConvention, o que significa que ela será acionada sempre que a hierarquia de herança mapeada para um tipo de entidade for alterada. Em seguida, a convenção localiza e configura a propriedade discriminador de cadeia de caracteres para a hierarquia.

Essa convenção é então usada chamando Add em ConfigureConventions:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Add(_ =>  new DiscriminatorLengthConvention1());
}

Observação

Em vez de adicionar uma instância da convenção diretamente, o método Add aceita uma fábrica para criar instâncias da convenção. Isso permite que a convenção use dependências do provedor de serviços interno do EF Core. Como essa convenção não tem dependências, o parâmetro do provedor de serviços é denominado _, indicando que ele nunca é usado.

Compilar o modelo e examinar o tipo de entidade Post mostra que isso funcionou - a propriedade discriminatória agora está configurada com um comprimento máximo de 24:

 Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Mas o que acontece se agora configurarmos explicitamente uma propriedade discriminatória diferente? Por exemplo:

modelBuilder.Entity<Post>()
    .HasDiscriminator<string>("PostTypeDiscriminator")
    .HasValue<Post>("Post")
    .HasValue<FeaturedPost>("Featured");

Examinando a exibição de depuração do modelo, descobrimos que o comprimento discriminatório não está mais configurado.

 PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw

Isso ocorre porque a propriedade discriminatória que configuramos em nossa convenção foi removida posteriormente quando o discriminador personalizado foi adicionado. Poderíamos tentar corrigir isso implementando outra interface em nossa convenção para reagir às mudanças discriminatórias, mas descobrir qual interface implementar não é fácil.

Felizmente, há uma abordagem mais fácil. Muitas vezes, não importa como o modelo se parece enquanto ele está sendo construído, desde que o modelo final esteja correto. Além disso, a configuração que queremos aplicar muitas vezes não precisa acionar outras convenções para reagir. Portanto, nossa convenção pode implementar IModelFinalizingConvention. As convenções de finalização de modelo são executadas após a conclusão de todos os outros modelos e, portanto, têm acesso ao estado quase final do modelo. Isso se opõe a convenções interativas que reagem a cada alteração de modelo e garantem que o modelo esteja atualizado em qualquer ponto da execução do método OnModelCreating. Uma convenção de finalização de modelo normalmente iterará sobre todo o modelo configurando elementos de modelo à medida que ele avança. Então, neste caso, vamos encontrar todos os discriminadores no modelo e configurá-lo:

public class DiscriminatorLengthConvention2 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                discriminatorProperty.Builder.HasMaxLength(24);
            }
        }
    }
}

Depois de construir o modelo com essa nova convenção, descobrimos que o comprimento discriminatório agora está configurado corretamente, embora tenha sido personalizado:

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Podemos ir mais longe e configurar o comprimento máximo para ter o comprimento do valor discriminatório mais longo:

public class DiscriminatorLengthConvention3 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                var maxDiscriminatorValueLength =
                    entityType.GetDerivedTypesInclusive().Select(e => ((string)e.GetDiscriminatorValue()!).Length).Max();

                discriminatorProperty.Builder.HasMaxLength(maxDiscriminatorValueLength);
            }
        }
    }
}

Agora, o comprimento máximo da coluna discriminatória é 8, que é o comprimento de "Destaque", o valor discriminatório mais longo em uso.

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)

Exemplo: comprimento padrão para todas as propriedades de cadeia de caracteres

Vamos examinar outro exemplo em que uma convenção de finalização pode ser usada – definindo um comprimento máximo padrão para qualquer propriedade de cadeia de caracteres. A convenção é bastante semelhante ao exemplo anterior:

public class MaxStringLengthConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
                     .SelectMany(
                         entityType => entityType.GetDeclaredProperties()
                             .Where(
                                 property => property.ClrType == typeof(string))))
        {
            property.Builder.HasMaxLength(512);
        }
    }
}

Essa convenção é bem simples. Ele localiza cada propriedade de cadeia de caracteres no modelo e define seu comprimento máximo como 512. Olhando na exibição de depuração nas propriedades de Post, vemos que todas as propriedades de cadeia de caracteres agora têm um comprimento máximo de 512.

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(512)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Observação

O mesmo pode ser feito pela configuração de pré-convenção, mas o uso de uma convenção permite filtrar ainda mais as propriedades aplicáveis e as Anotações de Dados para substituir a configuração.

Finalmente, antes de deixarmos este exemplo, o que acontece se usarmos o MaxStringLengthConvention e o DiscriminatorLengthConvention3 ao mesmo tempo? A resposta é que ela depende de qual ordem elas são adicionadas, uma vez que as convenções de finalização de modelo são executadas na ordem em que são adicionadas. Portanto, se MaxStringLengthConvention for adicionado por último, ele será executado por último e definirá o comprimento máximo da propriedade discriminatória como 512. Portanto, nesse caso, é melhor adicionar DiscriminatorLengthConvention3 último para que ele possa substituir o comprimento máximo padrão para apenas propriedades discriminatórias, deixando todas as outras propriedades de cadeia de caracteres como 512.

Substituindo uma convenção existente

Às vezes, em vez de remover completamente uma convenção existente, queremos substituí-la por uma convenção que faz basicamente a mesma coisa, mas com comportamento alterado. Isso é útil porque a convenção existente já implementará as interfaces de que precisa ser disparada adequadamente.

Exemplo: mapeamento de propriedades de aceitação

O EF Core mapeia todas as propriedades públicas de leitura/gravação por convenção. Isso pode não ser apropriado para a maneira como os tipos de entidade são definidos. Para alterar isso, podemos substituir o PropertyDiscoveryConvention por nossa própria implementação que não mapeia nenhuma propriedade, a menos que seja explicitamente mapeada em OnModelCreating ou marcada com um novo atributo chamado Persist:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}

Essa é a nova convenção:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

Dica

Ao substituir uma convenção interna, a nova implementação de convenção deve herdar da classe de convenção existente. Observe que algumas convenções têm implementações relacionais ou específicas do provedor, caso em que a nova implementação de convenção deve herdar da classe de convenção existente mais específica para o provedor de banco de dados em uso.

A convenção é então registrada usando o método Replace em ConfigureConventions:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
        serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
            serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}

Dica

Este é um caso em que a convenção existente tem dependências, representadas pelo objeto de dependência ProviderConventionSetBuilderDependencies. Eles são obtidos do provedor de serviços interno usando GetRequiredService e passados para o construtor da convenção.

Observe que essa convenção permite que os campos sejam mapeados (além das propriedades) desde que sejam marcados com [Persist]. Isso significa que podemos usar campos privados como chaves ocultas no modelo.

Por exemplo, considere os seguintes tipos de entidade:

public class LaundryBasket
{
    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    public bool IsClean { get; set; }

    public List<Garment> Garments { get; } = new();
}

public class Garment
{
    public Garment(string name, string color)
    {
        Name = name;
        Color = color;
    }

    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    [Persist]
    public string Name { get; }

    [Persist]
    public string Color { get; }

    public bool IsClean { get; set; }

    public LaundryBasket? Basket { get; set; }
}

O modelo criado a partir desses tipos de entidade é:

Model:
  EntityType: Garment
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Basket_id (no field, int?) Shadow FK Index
      Color (string) Required
      Name (string) Required
      TenantId (int) Required
    Navigations:
      Basket (LaundryBasket) ToPrincipal LaundryBasket Inverse: Garments
    Keys:
      _id PK
    Foreign keys:
      Garment {'Basket_id'} -> LaundryBasket {'_id'} ToDependent: Garments ToPrincipal: Basket ClientSetNull
    Indexes:
      Basket_id
  EntityType: LaundryBasket
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      TenantId (int) Required
    Navigations:
      Garments (List<Garment>) Collection ToDependent Garment Inverse: Basket
    Keys:
      _id PK

Normalmente, IsClean teria sido mapeado, mas como não está marcado [Persist], agora é tratado como uma propriedade não mapeada.

Dica

Essa convenção não pôde ser implementada como uma convenção de finalização de modelo porque existem convenções de finalização de modelo existentes que precisam ser executadas depois que a propriedade é mapeada para configurá-la ainda mais.

Considerações de implementação de convenções

O EF Core controla como cada parte da configuração foi feita. Isso é representado pela enumeração ConfigurationSource. Os diferentes tipos de configuração são:

  • Explicit: o elemento de modelo foi explicitamente configurado no OnModelCreating
  • DataAnnotation: o elemento de modelo foi configurado usando um atributo de mapeamento (também conhecido como anotação de dados) no tipo CLR
  • Convention: o elemento de modelo foi configurado por uma convenção de construção de modelo

As convenções nunca devem substituir a configuração marcada como DataAnnotation ou Explicit. Isso é obtido usando um construtorde convenções, por exemplo, o IConventionPropertyBuilder, que é obtido da propriedade Builder. Por exemplo:

property.Builder.HasMaxLength(512);

Chamar HasMaxLength no construtor de convenções só definirá o comprimento máximo se ele ainda não tiver sido configurado por um atributo de mapeamento ou em OnModelCreating.

Métodos de construtor como este também têm um segundo parâmetro: fromDataAnnotation. Defina isso como true se a convenção estiver fazendo a configuração em nome de um atributo de mapeamento. Por exemplo:

property.Builder.HasMaxLength(512, fromDataAnnotation: true);

Isso define o ConfigurationSource como DataAnnotation, o que significa que o valor agora pode ser substituído por mapeamento explícito em OnModelCreating, mas não por convenções de atributos que não sejam de mapeamento.

Se a configuração atual não puder ser substituída, o método retornará null, isso precisará ser contabilizado se você precisar executar outra configuração:

property.Builder.HasMaxLength(512)?.IsUnicode(false);

Observe que, se a configuração unicode não puder ser substituída, o comprimento máximo ainda será definido. Caso precise configurar as facetas somente quando ambas as chamadas forem bem-sucedidas, você poderá verificar isso preventivamente chamando CanSetMaxLength e CanSetIsUnicode:

public class MaxStringLengthNonUnicodeConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
                     .SelectMany(
                         entityType => entityType.GetDeclaredProperties()
                             .Where(
                                 property => property.ClrType == typeof(string))))
        {
            var propertyBuilder = property.Builder;
            if (propertyBuilder.CanSetMaxLength(512)
                && propertyBuilder.CanSetIsUnicode(false))
            {
                propertyBuilder.HasMaxLength(512)!.IsUnicode(false);
            }
        }
    }
}

Aqui, podemos ter certeza de que a chamada HasMaxLength não retornará null. Ainda é recomendável usar a instância do construtor retornada, HasMaxLength pois ela pode ser diferente de propertyBuilder.

Observação

Outras convenções não são disparadas imediatamente após uma convenção fazer uma alteração, elas são adiadas até que todas as convenções tenham terminado de processar a alteração atual.

IConventionContext

Todos os métodos de convenção também têm um parâmetro IConventionContext<TMetadata>. Ele fornece métodos que podem ser úteis em alguns casos específicos.

Exemplo: convenção NotMappedAttribute

Essa convenção procura por NotMappedAttribute em um tipo que é adicionado ao modelo e tenta remover esse tipo de entidade do modelo. No entanto, se o tipo de entidade for removido do modelo, outras convenções que implementam ProcessEntityTypeAdded não precisarão mais ser executadas. Isso pode ser feito chamando StopProcessing():

public virtual void ProcessEntityTypeAdded(
    IConventionEntityTypeBuilder entityTypeBuilder,
    IConventionContext<IConventionEntityTypeBuilder> context)
{
    var type = entityTypeBuilder.Metadata.ClrType;
    if (!Attribute.IsDefined(type, typeof(NotMappedAttribute), inherit: true))
    {
        return;
    }

    if (entityTypeBuilder.ModelBuilder.Ignore(entityTypeBuilder.Metadata.Name, fromDataAnnotation: true) != null)
    {
        context.StopProcessing();
    }
}

IConventionModel

Cada objeto do construtor passado para a convenção expõe uma propriedade Metadata que fornece um acesso de baixo nível aos objetos que compõem o modelo. Em particular, há métodos que permitem iterar sobre objetos específicos no modelo e aplicar a configuração comum a eles, como visto em Exemplo: comprimento padrão para todas as propriedades de cadeia de caracteres. Essa API é semelhante à IMutableModel mostrada na Configuração em massa.

Cuidado

É recomendável sempre executar a configuração chamando métodos no construtor exposto como a propriedade Builder, pois os construtores verificam se a configuração determinada substituiria algo que já foi especificado usando a API fluente ou anotações de dados.

Quando usar cada abordagem para configuração em massa

Use a API de Metadados quando:

  • A configuração precisa ser aplicada em um determinado momento e não reagir a alterações posteriores no modelo.
  • A velocidade de construção do modelo é muito importante. A API de metadados tem menos verificações de segurança e, portanto, pode ser um pouco mais rápida do que outras abordagens, no entanto, usar um modelo compilado produziria tempos de inicialização ainda melhores.

Use a Configuração do modelo de pré-convenção quando:

  • A condição de aplicabilidade é simples, pois depende apenas do tipo.
  • A configuração precisa ser aplicada a qualquer momento em que uma propriedade do tipo determinado é adicionada no modelo e substitui anotações e convenções de dados

Use convenções de finalização quando:

  • A condição de aplicabilidade é complexa.
  • A configuração não deve substituir o que é especificado por Anotações de Dados.

Use convenções interativas quando:

  • Várias convenções dependem umas das outras. As convenções de finalização são executadas na ordem em que foram adicionadas e, portanto, não podem reagir às alterações feitas por convenções de finalização posterior.
  • A lógica é compartilhada entre vários contextos. Convenções interativas são mais seguras do que outras abordagens.