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

Kon*_*nov 6 c# entity-framework-core ef-core-5.0

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

人们可能想知道为什么

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

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

最有可能的是不支持或不方便支持(或被“侵入”到模型中)的任何内容,EFC例如空间索引、多列PK、多列FK、自引用多个乘法表、定义的多个索引相同的列(有些是过滤器,有些只是常规索引),依此类推。EFEFKeyAttributeForeignKeyAttribute

没关系。我可以通过使用条件编译“覆盖”属性来轻松处理EFC 无法处理的问题,例如

#if EFCORE
using Key = MyKeyAttribute;
using Column = MyColumnAttribute;
using Index = MyIndexAttribute;
using ForeignKey = MyForeignKeyAttribute;
#endif
Run Code Online (Sandbox Code Playgroud)

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

让我发疯的是,无论我做什么,EFC都会设法“潜入”影子属性和类似的蹩脚东西。这使得我真的想关闭所有 EFC约定并手动构建整个模型。毕竟,我已经在这样做了,就像模型的 90% 一样。我宁愿要EFCthrow (带有有意义的错误消息),也不愿默默地做任何我不希望它做的事情。

按照@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();
}

Run Code Online (Sandbox Code Playgroud)

其中HasKeysHasColumnNames等是我[之前]编写的扩展方法,以继续使用多列 PK、F 等,这些方法不受支持,EFC并且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;
Run Code Online (Sandbox Code Playgroud)

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

(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)
Run Code Online (Sandbox Code Playgroud)

这意味着它RelationalModel是严重不完整的。

任何进一步的想法将受到高度赞赏。多谢!

Iva*_*oev 7

可以通过IModel手工构建

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

ModelBuilder()这里的要点是无参构造函数的使用

ModelBuilder没有约定地初始化类的新实例

然后使用该方法将其与上下文关联起来UseModel,例如在目标上下文OnConfiguring覆盖内

optionsBuilder.UseModel(CreateModel())
Run Code Online (Sandbox Code Playgroud)

通过这种方法,OnModelCreating不使用目标上下文。

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

警告:构建正确的模型通常需要约定。

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


更新:事实证明,警告中的“构建正确的模型通常需要约定”实际上意味着约定(至少其中一些)对于构建正确的运行时模型确实是强制性的,因为它们用于执行一些控制操作运行时行为。

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

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

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

public ConventionSet ModifyConventions (ConventionSet conventionSet);
Run Code Online (Sandbox Code Playgroud)

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

注册这样的插件并不那么容易,并且需要一堆管道(即使是样板代码)代码。但它是首选,因为它允许您删除特定的约定(请注意,约定类可以实现多个约定相关的接口,因此必须从多个ConventionSet列表中删除它),并且强制约定类具有额外的依赖项并使用 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它提供了类似于其他扩展的便捷Use扩展方法,因此您只需在配置期间调用它,例如在OnConfiguringoverride中

optionsBuilder.UseCustomConventionSet();
Run Code Online (Sandbox Code Playgroud)

或者用你的例子

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

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