Skip to content

Commit

Permalink
Add documentation for pre-convention model configuration
Browse files Browse the repository at this point in the history
Part of #3278
Fixes #571
  • Loading branch information
AndriySvyryd committed Dec 6, 2021
1 parent 3a32241 commit 60a8b6e
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 35 deletions.
4 changes: 2 additions & 2 deletions entity-framework/core/cli/powershell.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,13 @@ Parameters:

The [common parameters](#common-parameters) are listed above.

Example that uses the defaults and works if there is only one `DbContext` in the project:
The following example uses the defaults and works if there is only one `DbContext` in the project:

```powershell
Optimize-DbContext
```

Example that optimizes the model for the context with the specified name amd places it in a separate folder and namespace:
The following example optimizes the model for the context with the specified name and places it in a separate folder and namespace:

```powershell
Optimize-DbContext -OutputDir Models -Namespace BlogModels -Context BlogContext
Expand Down
71 changes: 71 additions & 0 deletions entity-framework/core/modeling/bulk-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Model Bulk Configuration - EF Core
description: How to apply bulk configuration during model building in Entity Framework Core
author: AndriySvyryd
ms.date: 11/05/2021
uid: core/modeling/bulk-configuration
---
# Model 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](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Modeling/BulkConfiguration) containing the code snippets presented below.

## Bulk configuration in OnModelCreating

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-csharp[Main](../../../samples/core/Modeling/BulkConfiguration/Currency.cs?name=Currency)]

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-csharp[Main](../../../samples/core/Modeling/BulkConfiguration/MetadataAPIContext.cs?name=MetadataAPI)]

[!code-csharp[Main](../../../samples/core/Modeling/BulkConfiguration/CurrencyConverter.cs?name=CurrencyConverter)]

### Drawbacks of the Metadata API

- 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](https://github.com/dotnet/efcore/issues/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.

## Pre-convention configuration

EF Core 6.0 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 that are then used by the model building conventions. Such configuration is applied by overriding `<xref:Microsoft.EntityFrameworkCore.DbContext.ConfigureConventions>` 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-csharp[Main](../../../samples/core/Modeling/BulkConfiguration/PreConventionContext.cs?name=CurrencyConversion)]

And this example shows how to configure some facets on all properties of type `string`:

[!code-csharp[Main](../../../samples/core/Modeling/BulkConfiguration/PreConventionContext.cs?name=StringFacets)]

> [!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:
>
> 1. Interface
> 2. Base type
> 3. Generic type definition
> 4. Non-nullable value type
> 5. Exact type
### Ignoring types

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-csharp[Main](../../../samples/core/Modeling/BulkConfiguration/PreConventionContext.cs?name=IgnoreInterface)]

### Default type mapping

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> to add or override a provider type mapping:

[!code-csharp[Main](../../../samples/core/Modeling/BulkConfiguration/PreConventionContext.cs?name=DefaultTypeMapping)]

### Limitations of pre-convention configuration

- Many aspects cannot be configured with this approach. [#6787](https://github.com/dotnet/efcore/issues/6787) will expand this to more types.
- Currently the configuration is only determined by the CLR type. [#20418](https://github.com/dotnet/efcore/issues/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.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ EF Core was designed to make it easy to work with data that follows a schema def

It is possible to access the properties that are not tracked by EF Core through a special property in [shadow-state](xref:core/modeling/shadow-properties) named `"__jObject"` that contains a `JObject` representing the data received from the store and data that will be stored:

[!code-csharp[Unmapped](../../../../samples/core/Cosmos/UnstructuredData/Sample.cs?highlight=23,24&name=Unmapped)]
[!code-csharp[Unmapped](../../../../samples/core/Cosmos/UnstructuredData/Sample.cs?highlight=21,22&name=Unmapped)]

```json
{
Expand Down
7 changes: 4 additions & 3 deletions entity-framework/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,13 @@
href: core/modeling/owned-entities.md
- name: Keyless entity types
href: core/modeling/keyless-entity-types.md
- name: Alternating models with same DbContext
href: core/modeling/dynamic-model.md
- name: Spatial data
displayName: GIS
href: core/modeling/spatial.md
#- name: Conventions
- name: Bulk configuration
href: core/modeling/bulk-configuration.md
- name: Alternating models with same DbContext
href: core/modeling/dynamic-model.md

- name: Manage database schemas
items:
Expand Down
14 changes: 14 additions & 0 deletions samples/core/Modeling/BulkConfiguration/BulkConfiguration.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>EFModeling.BulkConfiguration</RootNamespace>
<AssemblyName>EFModeling.BulkConfiguration</AssemblyName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0-rc.2.21480.5" />
</ItemGroup>

</Project>
15 changes: 15 additions & 0 deletions samples/core/Modeling/BulkConfiguration/Currency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace EFModeling.BulkConfiguration
{
#region Currency
public readonly struct Currency
{
public Currency(decimal amount)
=> Amount = amount;

public decimal Amount { get; }

public override string ToString()
=> $"${Amount}";
}
#endregion
}
16 changes: 16 additions & 0 deletions samples/core/Modeling/BulkConfiguration/CurrencyConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

namespace EFModeling.BulkConfiguration
{
#region CurrencyConverter
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
public CurrencyConverter()
: base(
v => v.Amount,
v => new Currency(v))
{
}
}
#endregion
}
33 changes: 33 additions & 0 deletions samples/core/Modeling/BulkConfiguration/MetadataAPIContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using Microsoft.EntityFrameworkCore;

namespace EFModeling.BulkConfiguration
{
public class MetadataAPIContext : DbContext
{
public DbSet<Product> Products { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region MetadataAPI
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));
}
}
}
#endregion
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFModeling.BulkConfiguration;Trusted_Connection=True")
.LogTo(Console.WriteLine, minimumLevel: Microsoft.Extensions.Logging.LogLevel.Information)
.EnableSensitiveDataLogging();
}
}
44 changes: 44 additions & 0 deletions samples/core/Modeling/BulkConfiguration/PreConventionContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace EFModeling.BulkConfiguration
{
public class PreConventionContext : DbContext
{
public DbSet<Product> Products { get; set; }

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
#region CurrencyConversion
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
#endregion

#region StringFacets
configurationBuilder
.Properties<string>()
.AreUnicode(false)
.HaveMaxLength(1024);
#endregion

#region IgnoreInterface
configurationBuilder
.IgnoreAny(typeof(IList<>));
#endregion

#region DefaultTypeMapping
configurationBuilder
.DefaultTypeMapping<Currency>()
.HasConversion<CurrencyConverter>();
#endregion
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFModeling.BulkConfiguration;Trusted_Connection=True")
.LogTo(Console.WriteLine, minimumLevel: Microsoft.Extensions.Logging.LogLevel.Information)
.EnableSensitiveDataLogging();
}
}
8 changes: 8 additions & 0 deletions samples/core/Modeling/BulkConfiguration/Product.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace EFModeling.BulkConfiguration
{
public class Product
{
public int Id { get; set; }
public Currency Price { get; set; }
}
}
51 changes: 51 additions & 0 deletions samples/core/Modeling/BulkConfiguration/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace EFModeling.BulkConfiguration
{
internal class Program
{
private static void Main()
{
Console.WriteLine("Sample showing bulk configuration of value conversion for a simple value object");
Console.WriteLine();

using (var context = new MetadataAPIContext())
{
RoundtripValue(context);
}

using (var context = new PreConventionContext())
{
RoundtripValue(context);
}

Console.WriteLine();
Console.WriteLine("Sample finished.");
}

private static void RoundtripValue(DbContext context)
{
Console.WriteLine("Using " + context.GetType().Name);
Console.WriteLine("Deleting and re-creating database...");
Console.WriteLine();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
Console.WriteLine();
Console.WriteLine("Done. Database is clean and fresh.");

var product = new Product { Price = new Currency(3.99m) };
context.Add(product);

Console.WriteLine("Save a new product with price: " + product.Price.Amount);
Console.WriteLine();

context.SaveChanges();

Console.WriteLine();
Console.WriteLine("Read the entity back with price: " + context.Set<Product>().Single().Price.Amount);
Console.WriteLine();
}
}
}
Loading

0 comments on commit 60a8b6e

Please sign in to comment.