如何关闭 Entity Framework Core 5 中的所有约定

How to turn off ALL conventions in Entity Framework Core 5

我想关闭 ALL(或至少大部分)Entity Framework Core 中的约定(我说的是 EF Core 5 或更高版本),然后“手工”构建整个模型。

有人可能想知道为什么

原因如下:我的任务是将几个大型遗留数据库从 Entity Framework 6 (EF) 迁移到 Entity Framework Core 5 (EFC)。这涉及数百个 table 和几个数据库。其中一些数据库是使用 Code First 方法创建的,而另一些只是第三方数据库,我们需要从 C# 代码查询和更新这些数据库。对于后一种数据库,我们必须完全匹配它们的架构。

由于问题的规模,代码的 EFEFC 风格必须共存,比方说,几个月。这可以通过使用条件编译轻松实现(见下文)。

EF 相比,EFC 最有可能不支持或不方便支持的任何内容(或被“黑入”EF 模型),例如空间索引、多-column KeyAttribute PKs,多列ForeignKeyAttribute FKs,多次自引用tables,在同一列上定义多个索引(有些是过滤器有些只是常规索引),等等等等。

没关系。我可以很容易地处理EFC无法通过使用条件编译“覆盖”属性来处理这个问题,例如

#if EFCORE
using Key = MyKeyAttribute;
using Column = MyColumnAttribute;
using Index = MyIndexAttribute;
using ForeignKey = MyForeignKeyAttribute;
#endif

然后为每个MyProject.csproj创建一个MyProject_EFC.csproj,其中定义了EFCORE,然后使用Reflection来“收集”所有这些自定义属性,然后使用EFC Fluent API 配置所有 EFC 做不到的事情。因此,遗留 (EF) 代码仍然会看到原始代码,例如KeyAttribute 然后遵循 EF 路线,而 EFC 代码将看不到属性,因为它们已被重新定义。因此,它不会抱怨。 我已经有了所有的代码,它可以工作,也许我会在某个时候把它放在这里或 GitHub,但不是今天.

让我发疯的是,无论我做什么,EFC 都会设法“潜入”影子属性和类似的糟糕事情。这到了我真的想关闭 ALL EFC 约定并手动构建整个模型的地步。毕竟,我已经在这样做了,比如 90% 的模型。我宁愿 EFC 抛出(带有有意义的错误消息),也不愿默默地做任何我不希望它做的事情。

按照@IvanStoev 的建议,我目前拥有的是:

public static IModel CreateModel<TContext, TContextInfo>(Action<ModelBuilder, TContextInfo>? modifier = null)
    where TContext : DbContext, ISwyfftDbContext
    where TContextInfo : ContextInfo<TContext>, new()
{
    var contextInfo = new TContextInfo();
    var modelBuilder = new ModelBuilder();

    modelBuilder
        .HasKeys<TContext, TContextInfo>(contextInfo)
        .HasColumnNames<TContext, TContextInfo>(contextInfo)
        .ToTables<TContext, TContextInfo>(contextInfo)
        .DisableCascadeDeletes()
        .HasDefaultValues<TContext, TContextInfo>(contextInfo)
        .HasComputedColumns<TContext, TContextInfo>(contextInfo)
        .HasForeignKeys<TContext, TContextInfo>(contextInfo)
        .HasDatabaseIndexes<TContext, TContextInfo>(contextInfo);

    modifier?.Invoke(modelBuilder, contextInfo);
    var model = modelBuilder.FinalizeRelationalModel();
    return model;
}

private static IModel FinalizeRelationalModel(this ModelBuilder modelBuilder)
{
    var model = modelBuilder.Model;
    var conventionModel = model as IConventionModel;
    var databaseModel = new RelationalModel(model);
    conventionModel.SetAnnotation(RelationalAnnotationNames.RelationalModel, databaseModel);
    return modelBuilder.FinalizeModel();
}

其中HasKeysHasColumnNames等是我[之前]写的扩展方法,用来继续使用多列PK、Fs等,[=16不支持=] 和 conventionModel.SetAnnotation(RelationalAnnotationNames.RelationalModel, databaseModel) 是强制性的,否则不会创建模型并且代码会因 NRE 而失败。

因此,当我将 CreateModel 插入 DbContextOptions 时:

public static DbContextOptions<TContext> GetDbContextOptions(string connectionString, Func<IModel> modelCreator) =>
    new DbContextOptionsBuilder<TContext>()
        .UseModel(modelCreator())
        .UseSqlServer(connectionString, x => x.UseNetTopologySuite())
        .Options;

并通过 运行 创建迁移,例如Add-Migration Initial 然后 ModelSnapshot 最终结果是正确的,没有垃圾影子属性,也没有其他废话 EFC 按照所有约定在这里或那里插入。但是,当我尝试查询任何 table 时,代码失败并显示:

(InvalidOperationException) Sequence contains no elements; 
Sequence contains no elements (   at System.Linq.ThrowHelper.ThrowNoElementsException()
   at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression..ctor(IEntityType entityType, ISqlExpressionFactory sqlExpressionFactory)
   at Microsoft.EntityFrameworkCore.Query.SqlExpressionFactory.Select(IEntityType entityType)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.CreateShapedQueryExpression(IEntityType entityType)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitExtension(Expression extensionExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitExtension(Expression extensionExpression)
   at System.Linq.Expressions.Expression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToQueryString(IQueryable source)

这意味着 RelationalModel 严重不完整。

任何进一步的想法将不胜感激。非常感谢!

可以通过手动构建 IModel

static IModel CreateModel(/* args */)
{
    var modelBuilder = new ModelBuilder();
    // Build the model using the modelBuilder
    // ...
    return modelBuilder.FinalizeModel();
}

这里的本质是无参数ModelBuilder()构造函数的使用

Initializes a new instance of the ModelBuilder class with no conventions

然后使用 UseModel 方法将其与上下文相关联,例如在目标上下文中 OnConfiguring override

optionsBuilder.UseModel(CreateModel())

使用此方法,不使用目标上下文的 OnModelCreating

这应该可以达到您的要求。但要注意使用的 ModelBuilder 构造函数的警告:

Warning: conventions are typically needed to build a correct model.

因此,您必须非常小心地显式映射所有内容。另一方面,EF Core 迁移在内部使用完全相同的方法(.designer.cs 文件中生成的方法 BuildTargetModel)在 classes 可能不存在或不存在的位置生成模型可能完全不同,所以如果使用得当,它应该是一个可行的选择。


更新: 结果是 "conventions are typically needed to build a correct model" 在警告中确实意味着约定(至少其中一些)对于构建正确的运行时模型确实强制性,因为它们用于执行一些控制运行时行为的操作。

最引人注目的是创建关系模型(表、列等)映射的 RelationalModelConvention 和创建提供者数据类型映射的 TypeMappingConvention。因此这两个是强制性的。但谁知道呢,可能还有更多。并且允许扩展添加自己的。

因此,在进一步阅读之前,请考虑使用具有所有约定的标准方法。严重地。 Fluent 配置具有更高的优先级(约定 < 数据注释 < fluent(显式)),因此如果您显式配置所有内容,则不应出现意外的阴影、鉴别器等。属性 问题。

现在,如果您想继续这条危险的道路,您应该创建所需的最少约定,或者更好的是,删除导致您出现问题的不需要的约定。 public EF Core 5.x 修改约定的方法是注册具有单一方法

的自定义 IConventionSetPlugin 实现
public ConventionSet ModifyConventions (ConventionSet conventionSet);

它允许您修改(替换、添加新的、删除)默认约定,甚至 return 一个全新的约定集。

注册这样的插件不是那么容易,需要一堆管道(即使是样板)代码。但它是首选,因为它允许您删除特定约定(请注意,约定 class 可以实现多个约定相关的接口,因此必须从多个 ConventionSet 列表中删除),以及强制性约定classes 具有额外的依赖项并使用 DI 容器来解析它们,因此从外部创建它们并不容易(如果不是不可能的话)。

话虽如此,这里是一个示例实现,它删除了所有约定,仅保留在 ModelFinalizingConventionsModelFinalizedConventions 中注册的约定,这似乎对于构建正常运行的运行时模型至关重要:

using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure
{
    public class CustomConventionSetPlugin : IConventionSetPlugin
    {
        public ConventionSet ModifyConventions(ConventionSet conventionSet)
        {
            conventionSet.EntityTypeAddedConventions.Clear();
            conventionSet.EntityTypeAnnotationChangedConventions.Clear();
            conventionSet.EntityTypeBaseTypeChangedConventions.Clear();
            conventionSet.EntityTypeIgnoredConventions.Clear();
            conventionSet.EntityTypeMemberIgnoredConventions.Clear();
            conventionSet.EntityTypePrimaryKeyChangedConventions.Clear();
            conventionSet.ForeignKeyAddedConventions.Clear();
            conventionSet.ForeignKeyAnnotationChangedConventions.Clear();
            conventionSet.ForeignKeyDependentRequirednessChangedConventions.Clear();
            conventionSet.ForeignKeyRequirednessChangedConventions.Clear();
            conventionSet.ForeignKeyUniquenessChangedConventions.Clear();
            conventionSet.IndexAddedConventions.Clear();
            conventionSet.IndexAnnotationChangedConventions.Clear();
            conventionSet.IndexRemovedConventions.Clear();
            conventionSet.IndexUniquenessChangedConventions.Clear();
            conventionSet.KeyAddedConventions.Clear();
            conventionSet.KeyAnnotationChangedConventions.Clear();
            conventionSet.KeyRemovedConventions.Clear();
            conventionSet.ModelAnnotationChangedConventions.Clear();
            //conventionSet.ModelFinalizedConventions.Clear();
            //conventionSet.ModelFinalizingConventions.Clear();
            conventionSet.ModelInitializedConventions.Clear();
            conventionSet.NavigationAddedConventions.Clear();
            conventionSet.NavigationAnnotationChangedConventions.Clear();
            conventionSet.NavigationRemovedConventions.Clear();
            conventionSet.PropertyAddedConventions.Clear();
            conventionSet.PropertyAnnotationChangedConventions.Clear();
            conventionSet.PropertyFieldChangedConventions.Clear();
            conventionSet.PropertyNullabilityChangedConventions.Clear();
            conventionSet.PropertyRemovedConventions.Clear();
            conventionSet.SkipNavigationAddedConventions.Clear();
            conventionSet.SkipNavigationAnnotationChangedConventions.Clear();
            conventionSet.SkipNavigationForeignKeyChangedConventions.Clear();
            conventionSet.SkipNavigationInverseChangedConventions.Clear();
            conventionSet.SkipNavigationRemovedConventions.Clear();
            return conventionSet;
        }
    }
}

// Boilerplate for regigistering the plugin

namespace Microsoft.EntityFrameworkCore.Infrastructure
{
    public class CustomConventionSetOptionsExtension : IDbContextOptionsExtension
    {
        public CustomConventionSetOptionsExtension() { }
        ExtensionInfo info;
        public DbContextOptionsExtensionInfo Info => info ?? (info = new ExtensionInfo(this));
        public void Validate(IDbContextOptions options) { }
        public void ApplyServices(IServiceCollection services)
            => services.AddSingleton<IConventionSetPlugin, CustomConventionSetPlugin>();
        sealed class ExtensionInfo : DbContextOptionsExtensionInfo
        {
            public ExtensionInfo(CustomConventionSetOptionsExtension extension) : base(extension) { }
            public override bool IsDatabaseProvider => false;
            public override string LogFragment => string.Empty;
            public override void PopulateDebugInfo(IDictionary<string, string> debugInfo) { }
            public override long GetServiceProviderHashCode() => 1234;
        }
    }
}

namespace Microsoft.EntityFrameworkCore
{
    public static partial class CustomDbContextOptionsExtensions
    {
        public static DbContextOptionsBuilder UseCustomConventionSet(this DbContextOptionsBuilder optionsBuilder)
        {
            if (optionsBuilder.Options.FindExtension<CustomConventionSetOptionsExtension>() == null)
                ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(new CustomConventionSetOptionsExtension());
            return optionsBuilder;
        }
    }
}

它提供了一个方便的 Use 扩展方法,类似于其他扩展,所以你只需要在配置期间调用它,例如在 OnConfiguring 覆盖

optionsBuilder.UseCustomConventionSet();

或者用你的例子

public static DbContextOptions<TContext> GetDbContextOptions(string connectionString, Func<IModel> modelCreator) =>
    new DbContextOptionsBuilder<TContext>()
        .UseSqlServer(connectionString, x => x.UseNetTopologySuite())
        .UseCustomConventionSet()
        .Options;

OnConfiguring 是首选,因为这与数据库提供程序无关,也不像最初的建议那样使用外部模型创建(和 UseModel)——流畅的配置回到 OnModelCreating 覆盖。