title | description | author | ms.date | uid |
---|---|---|---|---|
Model Bulk Configuration - EF Core |
How to apply bulk configuration during model building in Entity Framework Core via Metadata API, conventions or pre-convention configuration. |
AndriySvyryd |
11/11/2022 |
core/modeling/bulk-configuration |
When an aspect needs to be configured in the same way across multiple entity types, the following techniques allow to reduce code duplication and consolidate the logic.
See the full sample project containing the code snippets presented below.
Every builder object returned from xref:Microsoft.EntityFrameworkCore.ModelBuilder exposes a xref:Microsoft.EntityFrameworkCore.ModelBuilder.Model or Metadata
property that provides a low-level access to the objects that comprise the model. In particular, there are methods that allow you to iterate over specific objects in the model and apply common configuration to them.
In the following example the model contains a custom value type Currency
:
[!code-csharpMain]
Properties of this type are not discovered by default as the current EF provider doesn't know how to map it to a database type. This snippet of OnModelCreating
adds all properties of the type Currency
and configures a value converter to a supported type - decimal
:
[!code-csharpMain]
[!code-csharpMain]
- Unlike Fluent API, every modification to the model needs to be done explicitly. For example, if some of the
Currency
properties were configured as navigations by a convention then you need to first remove the navigation referencing the CLR property before adding an entity type property for it. #9117 will improve this. - The conventions run after each change. If you remove a navigation discovered by a convention then the convention will run again and could add it back. To prevent this from happening you would need to either delay the conventions until after the property is added by calling xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext.DelayConventions and later disposing the returned object or to mark the CLR property as ignored using xref:Microsoft.EntityFrameworkCore.Metadata.IMutableModel.AddIgnored%2A.
- Entity types might be added after this iteration happens and the configuration won't be applied to them. This can usually be prevented by placing this code at the end of
OnModelCreating
, but if you have two interdependent sets of configurations there might not be an order that will allow them to be applied consistently.
EF Core allows the mapping configuration to be specified once for a given CLR type; that configuration is then applied to all properties of that type in the model as they are discovered. This is called "pre-convention model configuration", since it configures aspects of the model before the model building conventions are allowed to run. Such configuration is applied by overriding xref:Microsoft.EntityFrameworkCore.DbContext.ConfigureConventions%2A on the type derived from xref:Microsoft.EntityFrameworkCore.DbContext.
This example shows how configure all properties of type Currency
to have a value converter:
[!code-csharpMain]
And this example shows how to configure some facets on all properties of type string
:
[!code-csharpMain]
Note
The type specified in a call from ConfigureConventions
can be a base type, an interface or a generic type definition. All matching configurations will be applied in order from the least specific:
- Interface
- Base type
- Generic type definition
- Non-nullable value type
- Exact type
Important
Pre-convention configuration is equivalent to explicit configuration that is applied as soon as a matching object is added to the model. It will override all conventions and Data Annotations. For example, with the above configuration all string foreign key properties will be created as non-unicode with MaxLength
of 1024, even when this doesn't match the principal key.
Pre-convention configuration also allows to ignore a type and prevent it from being discovered by conventions either as an entity type or as a property on an entity type:
[!code-csharpMain]
Generally, EF is able to translate queries with constants of a type that is not supported by the provider, as long as you have specified a value converter for a property of this type. However, in queries that don't involve any properties of this type, there is no way for EF to find the correct value converter. In this case, it's possible to call xref:Microsoft.EntityFrameworkCore.ModelConfigurationBuilder.DefaultTypeMapping%2A to add or override a provider type mapping:
[!code-csharpMain]
- Many aspects cannot be configured with this approach. #6787 will expand this to more types.
- Currently the configuration is only determined by the CLR type. #20418 would allow custom predicates.
- This configuration is performed before a model is created. If there are any conflicts that arise when applying it, the exception stack trace will not contain the
ConfigureConventions
method, so it might be harder to find the cause.
Note
Custom model building conventions were introduced in EF Core 7.0.
EF Core model building conventions are classes that contain logic that is triggered based on changes being made to the model as it is being built. This keeps the model up-to-date as explicit configuration is made, mapping attributes are applied, and other conventions run. To participate in this, every convention implements one or more interfaces which determine when the corresponding method will be triggered. For example, a convention that implements xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IEntityTypeAddedConvention will be triggered whenever a new entity type is added to the model. Likewise, a convention that implements both xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyAddedConvention and xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IKeyAddedConvention will be triggered whenever either a key or a foreign key is added to the model.
Model building conventions are a powerful way to control the model configuration, but can be complex and hard to get right. In many cases, the pre-convention model configuration can be used instead to easily specify common configuration for properties and types.
The table-per-hierarchy inheritance mapping strategy requires a discriminator column to specify which type is represented in any given row. By default, EF uses an unbounded string column for the discriminator, which ensures that it will work for any discriminator length. However, constraining the maximum length of discriminator strings can make for more efficient storage and queries. Let's create a new convention that will do that.
EF Core model building conventions are triggered based on changes being made to the model as it is being built. This keeps the model up-to-date as explicit configuration is made, mapping attributes are applied, and other conventions run. To participate in this, every convention implements one or more interfaces which determine when the convention will be triggered. For example, a convention that implements xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IEntityTypeAddedConvention will be triggered whenever a new entity type is added to the model. Likewise, a convention that implements both xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyAddedConvention and xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IKeyAddedConvention will be triggered whenever either a key or a foreign key is added to the model.
Knowing which interfaces to implement can be tricky, since configuration made to the model at one point may be changed or removed at a later point. For example, a key may be created by convention, but then later replaced when a different key is configured explicitly.
Let's make this a bit more concrete by making a first attempt at implementing the discriminator-length convention:
[!code-csharpDiscriminatorLengthConvention1]
This convention implements xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IEntityTypeBaseTypeChangedConvention, which means it will be triggered whenever the mapped inheritance hierarchy for an entity type is changed. The convention then finds and configures the string discriminator property for the hierarchy.
This convention is then used by calling xref:Microsoft.EntityFrameworkCore.Metadata.Builders.ConventionSetBuilder.Add%2A in ConfigureConventions
:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new DiscriminatorLengthConvention1());
}
Note
Rather than adding an instance of the convention directly, the Add
method accepts a factory for creating instances of the convention. This allows the convention to use dependencies from the EF Core internal service provider. Since this convention has no dependencies, the service provider parameter is named _
, indicating that it is never used.
Building the model and looking at the Post
entity type shows that this has worked - the discriminator property is now configured to with a maximum length of 24:
Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
But what happens if we now explicitly configure a different discriminator property? For example:
modelBuilder.Entity<Post>()
.HasDiscriminator<string>("PostTypeDiscriminator")
.HasValue<Post>("Post")
.HasValue<FeaturedPost>("Featured");
Looking at the debug view of the model, we find that the discriminator length is no longer configured.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw
This is because the discriminator property that we configured in our convention was later removed when the custom discriminator was added. We could attempt to fix this by implementing another interface on our convention to react to the discriminator changes, but figuring out which interface to implement is not easy.
Fortunately, there is an easier approach. A lot of the time, it doesn't matter what the model looks like while it is being built, as long as the final model is correct. In addition, the configuration we want to apply often does not need to trigger other conventions to react. Therefore, our convention can implement xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IModelFinalizingConvention. Model finalizing conventions run after all other model building is complete, and so have access to the near-final state of the model. This is opposed to interactive conventions that react to each model change and make sure that the model is up-to-date at any point of the OnModelCreating
method execution. A model finalizing convention will typically iterate over the entire model configuring model elements as it goes. So, in this case, we will find every discriminator in the model and configure it:
[!code-csharpDiscriminatorLengthConvention2]
After building the model with this new convention, we find that the discriminator length is now configured correctly even though it has been customized:
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
We can go one step further and configure the max length to be the length of the longest discriminator value:
[!code-csharpDiscriminatorLengthConvention3]
Now the discriminator column max length is 8, which is the length of "Featured", the longest discriminator value in use.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)
Let's look at another example where a finalizing convention can be used - setting a default maximum length for any string property. The convention looks quite similar to the previous example:
[!code-csharpMaxStringLengthConvention]
This convention is pretty simple. It finds every string property in the model and sets its max length to 512. Looking in the debug view at the properties for Post
, we see that all the string properties now have a max length of 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)
Note
The same can be accomplished by pre-convention configuration, but using a convention allows to further filter applicable properties and for Data Annotations to override the configuration.
Finally, before we leave this example, what happens if we use both the MaxStringLengthConvention
and DiscriminatorLengthConvention3
at the same time? The answer is that it depends which order they are added, since model finalizing conventions run in the order they are added. So if MaxStringLengthConvention
is added last, then it will run last, and it will set the max length of the discriminator property to 512. Therefore, in this case, it is better to add DiscriminatorLengthConvention3
last so that it can override the default max length for just discriminator properties, while leaving all other string properties as 512.
Sometimes rather than removing an existing convention completely we instead want to replace it with a convention that does basically the same thing, but with changed behavior. This is useful because the existing convention will already implement the interfaces it needs to be triggered appropriately.
EF Core maps all public read-write properties by convention. This might not be appropriate for the way your entity types are defined. To change this, we can replace the xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.PropertyDiscoveryConvention with our own implementation that doesn't map any property unless it is explicitly mapped in OnModelCreating
or marked with a new attribute called Persist
:
[!code-csharpPersistAttribute]
Here is the new convention:
[!code-csharpAttributeBasedPropertyDiscoveryConvention]
Tip
When replacing a built-in convention, the new convention implementation should inherit from the existing convention class. Note that some conventions have relational or provider-specific implementations, in which case the new convention implementation should inherit from the most specific existing convention class for the database provider in use.
The convention is then registered using the xref:Microsoft.EntityFrameworkCore.Metadata.Builders.ConventionSetBuilder.Replace%2A method in ConfigureConventions
:
[!code-csharpReplaceConvention]
Tip
This is a case where the existing convention has dependencies, represented by the ProviderConventionSetBuilderDependencies
dependency object. These are obtained from the internal service provider using GetRequiredService
and passed to the convention constructor.
Notice that this convention allows fields to be mapped (in addition to properties) so long as they are marked with [Persist]
. This means we can use private fields as hidden keys in the model.
For example, consider the following entity types:
[!code-csharpLaundryBasket]
The model built from these entity types is:
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
Normally, IsClean
would have been mapped, but since it is not marked with [Persist]
, it is now treated as an un-mapped property.
Tip
This convention could not be implemented as a model finalizing convention because there are existing model finalizing conventions that need to run after the property is mapped to further configure it.
EF Core keeps track of how every piece of configuration was made. This is represented by the xref:Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource enum. The different kinds of configuration are:
Explicit
: The model element was explicitly configured inOnModelCreating
DataAnnotation
: The model element was configured using a mapping attribute (aka data annotation) on the CLR typeConvention
: The model element was configured by a model building convention
Conventions should never override configuration marked as DataAnnotation
or Explicit
. This is achieved by using a convention builder, for example, the xref:Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder, which is obtained from the xref:Microsoft.EntityFrameworkCore.Metadata.IConventionProperty.Builder property. For example:
property.Builder.HasMaxLength(512);
Calling HasMaxLength
on the convention builder will only set the max length if it was not already configured by a mapping attribute or in OnModelCreating
.
Builder methods like this also have a second parameter: fromDataAnnotation
. Set this to true
if the convention is making the configuration on behalf of a mapping attribute. For example:
property.Builder.HasMaxLength(512, fromDataAnnotation: true);
This sets the ConfigurationSource
to DataAnnotation
, which means that the value can now be overridden by explicit mapping on OnModelCreating
, but not by non-mapping attribute conventions.
If the current configuration can't be overridden then the method will return null
, this needs to be accounted for if you need to perform further configuration:
property.Builder.HasMaxLength(512)?.IsUnicode(false);
Notice that if the unicode configuration can't be overridden the max length will still be set. In case when you need to configure the facets only when both calls succeed then you can preemptively check this by calling xref:Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder.CanSetMaxLength%2A and xref:Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder.CanSetIsUnicode%2A:
[!code-csharpMaxStringLengthNonUnicodeConvention]
Here we can be sure that the call to HasMaxLength
will not return null
. It is still recommended to use the builder instance returned from HasMaxLength
as it might be different from propertyBuilder
.
Note
Other conventions are not triggered immediately after a convention makes a change, they are delayed until all conventions have finished processing the current change.
All convention methods also have an xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext%601 parameter. It provides methods that could be useful in some specific cases.
This convention looks for xref:System.ComponentModel.DataAnnotations.Schema.NotMappedAttribute on a type that is added to the model and tries to remove that entity type from the model. But if the entity type is removed from the model then any other conventions that implement ProcessEntityTypeAdded
no longer need to be run. This can be accomplished by calling xref:Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext.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();
}
}
Every builder object passed to the convention exposes a xref:Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionModelBuilder.Metadata property that provides a low-level access to the objects that comprise the model. In particular, there are methods that allow you to iterate over specific objects in the model and apply common configuration to them as seen in Example: Default length for all string properties. This API is similar to xref:Microsoft.EntityFrameworkCore.Metadata.IMutableModel shown in Bulk configuration.
Caution
It is advised to always perform configuration by calling methods on the builder exposed as the xref:Microsoft.EntityFrameworkCore.Metadata.IConventionModel.Builder property, because the builders check whether the given configuration would override something that was already specified using Fluent API or Data Annotations.
Use Metadata API when:
- The configuration needs to be applied at a certain time and not react to later changes in the model.
- The model building speed is very important. Metadata API has fewer safety checks and thus can be slightly faster than other approaches, however using a Compiled model would yield even better startup times.
Use Pre-convention model configuration when:
- The applicability condition is simple as it only depends on the type.
- The configuration needs to be applied at any point a property of the given type is added in the model and overrides Data Annotations and conventions
Use Finalizing Conventions when:
- The applicability condition is complex.
- The configuration shouldn't override what is specified by Data Annotations.
Use Interactive Conventions when:
- Multiple conventions depend on each other. Finalizing conventions run in the order they were added and therefore can't react to changes made by later finalizing conventions.
- The logic is shared between several contexts. Interactive conventions are safer than other approaches.