简体   繁体   English

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

[英]How to turn off ALL conventions in Entity Framework Core 5

I want to turn off ALL (or at least most of) conventions in Entity Framework Core (and I am talking about EF Core 5 or above) and then build the whole model "by hands".我想关闭 Entity Framework Core 中的所有(或至少大部分)约定(我说的是 EF Core 5 或更高版本),然后“手动”构建整个 model。

One may wonder why .有人可能想知道为什么

Here is why : I have a task to migrate several large legacy databases from Entity Framework 6 ( EF ) to Entity Framework Core 5 ( EFC ).原因如下:我的任务是将几个大型遗留数据库从 Entity Framework 6 ( EF ) 迁移到 Entity Framework Core 5 ( EFC )。 This involves many hundreds of tables and several databases.这涉及数百个表和几个数据库。 Some of these databases are created using a Code First approach and some are just third party databases, which we need to query and update from C# code.其中一些数据库是使用 Code First 方法创建的,而一些只是第三方数据库,我们需要从 C# 代码中查询和更新这些数据库。 For the latter databases we must match their schema exactly.对于后面的数据库,我们必须完全匹配它们的模式。

Due to the size of the problem both EF and EFC flavors of the code must coexist for, let's say, several months.由于问题的规模, EFEFC风格的代码必须共存,比如说几个月。 This can be easily achieved by using conditional compilation (see below).这可以通过使用条件编译轻松实现(见下文)。

Most likely anything that is not supported or is inconveniently supported in EFC in comparison to EF (or was "hacked" into EF models), like spatial indexes, multi-column KeyAttribute PKs, multi-column ForeignKeyAttribute FKs, self-referencing multiple times tables, multiple indexes defined on the same columns (some are filters and some are just regular indexes), and so on and so forth is there.EF相比,很可能在EFC中不支持或不方便支持的任何内容(或被“入侵”到EF模型中),例如空间索引、多列KeyAttribute PK、多列ForeignKeyAttribute FK、自引用多个时间表,在同一列上定义的多个索引(有些是过滤器,有些只是常规索引),依此类推。

That's fine.没关系。 I can easily deal with EFC inability to deal with that by "overriding" the attributes using conditional compilation, eg我可以通过使用条件编译“覆盖”属性来轻松处理EFC无法处理的问题,例如

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

then for each MyProject.csproj create a MyProject_EFC.csproj where EFCORE is defined, then use Reflection to "collect" all these custom attributes, and then use EFC Fluent API to configure all that stuff that EFC can't do.然后为每个MyProject.csproj创建一个定义EFCOREMyProject_EFC.csproj ,然后使用反射“收集”所有这些自定义属性,然后使用EFC Fluent API 配置所有EFC不能做的东西。 So, the legacy ( EF ) code will still see the originals, eg KeyAttribute and then follow the EF route, whereas the EFC code won't see the attributes because they were redefined.因此,遗留 ( EF ) 代码仍将看到原始代码,例如KeyAttribute ,然后遵循EF路线,而EFC代码将看不到属性,因为它们被重新定义。 And so, it won't complain.所以,它不会抱怨。 I already have all that code, it works and, perhaps, I will put it here or in GitHub at some point, but not today .我已经有了所有的代码,它可以工作,也许我会把它放在这里或在某个时候放在 GitHub 中,但不是今天

What drives me nuts is that no matter what I do, EFC manages to "sneak in" shadow properties and similar crappy things.让我发疯的是,无论我做什么, EFC都会设法“潜入”阴影属性和类似的蹩脚的东西。 This gets to the point that I really want to turn off ALL EFC conventions and build the whole model by hands.这到了我真的想关闭所有EFC约定并手动构建整个 model 的地步。 After all, I am already doing that, like for the 90% of the model.毕竟,我已经在这样做了,就像 90% 的 model 一样。 I would rather want EFC throw (with a meaningful error message) than silently do anything that I don't expect it to do.我宁愿希望EFC抛出(带有有意义的错误消息),也不愿默默地做任何我不希望它做的事情。

Following the advice of @IvanStoev here is what I currently have:遵循@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();
}

where HasKeys , HasColumnNames , etc. are extension methods that I wrote [earlier] to keep using multi-column PKs, Fs, etc., which are not supported by EFC and conventionModel.SetAnnotation(RelationalAnnotationNames.RelationalModel, databaseModel) is mandatory as otherwise the model is not created and the code fails with NRE.其中HasKeysHasColumnNames等是我 [earlier] 为了继续使用EFC不支持的多列 PK、Fs 等而编写的扩展方法, conventionModel.SetAnnotation(RelationalAnnotationNames.RelationalModel, databaseModel)是强制性的,否则model 未创建,代码因 NRE 而失败。

So, when I stick in this CreateModel into DbContextOptions :因此,当我将此CreateModel插入DbContextOptions

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

and create a migration by running eg Add-Migration Initial then ModelSnapshot finally comes out correct with no garbage shadow properties, and no other crap that EFC with all conventions inserts here or there.并通过运行例如Add-Migration Initial来创建迁移,然后ModelSnapshot最终结果是正确的,没有垃圾阴影属性,并且没有其他带有所有约定的EFC在这里或那里插入的废话。 However, when I try to query any table, the code fails with:但是,当我尝试查询任何表时,代码失败并显示:

(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)

which means that the RelationalModel is severely incomplete.这意味着RelationalModel严重不完整。

Any further ideas will be highly appreciated.任何进一步的想法将不胜感激。 Thanks a lot!非常感谢!

It's possible by building the IModel by hand可以通过手动构建IModel

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

The essential here is the usage of the parameterless ModelBuilder() constructor which这里最重要的是使用无参数的ModelBuilder()构造函数,它

Initializes a new instance of the ModelBuilder class with no conventions初始化没有约定的ModelBuilder器 class 的新实例

Then you associate it with the context using the UseModel method, for instance inside the target context OnConfiguring override然后使用UseModel方法将它与上下文相关联,例如在目标上下文OnConfiguring覆盖中

optionsBuilder.UseModel(CreateModel())

With this approach, OnModelCreating of the target context is not used.使用这种方法,不使用目标上下文的OnModelCreating

This should achieve what you are asked for.这应该可以达到您的要求。 But beware of the warning for the used ModelBuilder constructor:但请注意使用的ModelBuilder构造函数的警告:

Warning: conventions are typically needed to build a correct model.警告:通常需要约定来构建正确的 model。

So you must be very careful to map everything explicitly.所以你必须非常小心地明确 map 一切。 From the other side, the exact same approach is used internally by EF Core migrations (the generated method BuildTargetModel inside .designer.cs file) to generate model at point where the classes might not exist or could be totally different, so it should be a viable option when used properly.另一方面,EF Core 迁移在内部使用完全相同的方法(在.designer.cs文件中生成的方法BuildTargetModel )在类可能不存在或可能完全不同的位置生成 model,因此它应该是正确使用时可行的选择。


Update: It turns out that "conventions are typically needed to build a correct model" in the warning really means that conventions (at least some of them) are really mandatory for building correct runtime model, because they are used to perform some actions which control the runtime behaviors.更新:事实证明,警告中的“通常需要约定来构建正确的模型”实际上意味着约定(至少其中一些)对于构建正确的运行时 model 确实是强制性的,因为它们用于执行一些控制操作运行时行为。

Most noticeable are RelationalModelConvention which creates the relational model (tables, columns etc.) mappings, and TypeMappingConvention which creates provider data type mappings.最引人注目的是创建RelationalModelConvention model(表、列等)映射的 RelationalModelConvention 和创建提供程序数据类型映射的TypeMappingConvention Hence these two are mandatory.因此,这两个是强制性的。 But who knows, there could be more.但谁知道呢,可能还有更多。 And extensions are allowed to add their own.并且允许扩展添加自己的。

So before reading further, consider using the standard approach with all conventions.因此,在进一步阅读之前,请考虑使用所有约定的标准方法。 Seriously.严重地。 Fluent configuration has higher priority (conventions < data annotations < fluent (explicit)), so if you explicitly configure everything, you shouldn't be getting unexpected shadow, discriminator etc. property issues.流畅的配置具有更高的优先级(约定 < 数据注释 < 流畅(显式)),因此如果您显式配置所有内容,您不应该遇到意外的影子、鉴别器等属性问题。

Now, if you want to continue the dangerous path, you should create the minimal conventions needed, or even better, remove the not needed conventions which are causing you problems.现在,如果您想继续危险的道路,您应该创建所需的最小约定,或者更好的是,删除导致您出现问题的不需要的约定。 The public EF Core 5.x way of modifying conventions is to register custom IConventionSetPlugin implementation which has single method修改约定的公共 EF Core 5.x 方法是注册具有单一方法的自定义IConventionSetPlugin实现

public ConventionSet ModifyConventions (ConventionSet conventionSet);

which allows you to modify (replace, add new, remove) the default conventions, or even return a brand new convention set.它允许您修改(替换、添加、删除)默认约定,甚至返回一个全新的约定集。

Registering such plugin is not so easy and requires a bunch of plumbing (even though boilerplate) code.注册这样的插件并不容易,需要一堆管道(即使是样板)代码。 But it is preferred because it allows you to remove specific conventions (just note that the convention class can implement several convention related interfaces, so it must be removed from several ConventionSet lists), and also the mandatory convention classes have additional dependencies and use DI container for resolving them, so it's not easy (if not impossible) to create them from outside.但它是首选,因为它允许您删除特定的约定(只需注意约定 class 可以实现多个约定相关的接口,因此必须从多个ConventionSet列表中删除),并且强制约定类具有额外的依赖关系并使用 DI 容器为了解决它们,所以从外部创建它们并不容易(如果不是不可能的话)。

With that being said, here is a sample implementation which removes all conventions, keeping only the ones registered in ModelFinalizingConventions and ModelFinalizedConventions which seems to be essential for building properly functioning runtime model:话虽如此,这里是一个示例实现,它删除了所有约定,只保留在ModelFinalizingConventionsModelFinalizedConventions中注册的那些,这似乎对于构建正常运行的运行时 model 至关重要:

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;
        }
    }
}

It provides a convenient Use extension method similar to other extensions, so all you need is to call it during the configuration, eg in OnConfiguring override它提供了一个方便的Use扩展方法,类似于其他扩展,所以你只需要在配置过程中调用它,例如在OnConfiguring override

optionsBuilder.UseCustomConventionSet();

or with your example或以您的示例

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

OnConfiguring is preferred though, since this has nothing to do with database provider, and also does not use external model creation (and UseModel ) as with the original suggestion - the fluent configuration goes back to OnModelCreating override.不过,首选OnConfiguring ,因为这与数据库提供程序无关,并且也不像最初的建议那样使用外部 model 创建(和UseModel )——流畅的配置回到OnModelCreating覆盖。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM