【问题标题】:How to turn off ALL conventions in Entity Framework Core 5如何关闭 Entity Framework Core 5 中的所有约定
【发布时间】:2021-08-17 03:52:33
【问题描述】:

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

可能有人想知道为什么

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

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

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

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

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

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

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

遵循@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 等,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;

并通过运行例如创建迁移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)

这意味着RelationalModel 严重不完整。

任何进一步的想法都将受到高度赞赏。非常感谢!

【问题讨论】:

    标签: c# entity-framework-core ef-core-5.0


    【解决方案1】:

    可以手动构建IModel

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

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

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

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

    optionsBuilder.UseModel(CreateModel())
    

    使用这种方法,目标上下文的OnModelCreating 不会被使用。

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

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

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


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

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

    因此,在进一步阅读之前,请考虑对所有约定使用标准方法。严重地。 Fluent 配置具有更高的优先级(约定

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

    public ConventionSet ModifyConventions (ConventionSet conventionSet);
    

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

    注册这样的插件并不容易,并且需要一堆管道(即使是样板)代码。但它是首选的,因为它允许你删除特定的约定(只是注意约定类可以实现几个约定相关的接口,所以必须从几个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;
            }
        }
    }
    

    它提供了一个方便的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 覆盖.

    【讨论】:

    • 谢谢。几天后我会试一试,并就结果提出建议。
    • 我更新了问题。 RelationalModel 不完整,但 ModelSnapshot 最终正确。你对RelationalModel 有什么想法吗?谢谢。
    • @KonstantinKonstantinov 查看更新。找不到更可靠的方法,即使建议的方法也不能保证有效。但至少(经过测试)它允许执行查询,这在原始模型以及添加的关系模型中是不可能的。
    • 另一个必须遵守的约定是EntityTypeAddedConventions。没有它,EFC 无法转换 byte[] 数据类型。还有PropertyAnnotationChangedConventions,负责为非PK列添加标识。但是,它还为所有string(不是string?)属性添加了IsRequired,这是错误的。所以,我最终只保留了 4 个约定。我还使用了.ReplaceService&lt;IProviderConventionSetBuilder, EmptyCustomSetBuilder&gt;(),其中EmptyCustomSetBuilder : SqlServerConventionSetBuilder 覆盖CreateConventionSet 并清除所有不需要的约定。
    • 这是另一种方式。但不建议这样做,因为它被标记为内部,更重要的是,绑定到 SqlServer 提供程序,而插件方法与数据库无关。至于要保留哪些约定,我已经写了我的意见以“全部保留”:) 或者只删除一些您确实知道不需要的约定。
    猜你喜欢
    • 2021-06-24
    • 1970-01-01
    • 2018-03-31
    • 1970-01-01
    • 2021-04-09
    • 2019-04-19
    • 2017-02-06
    • 2018-09-28
    • 1970-01-01
    相关资源
    最近更新 更多