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".
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). 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. 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. 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.
That's fine. I can easily deal with EFC inability to deal with that by "overriding" the attributes using conditional compilation, e.g.
#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. So, the legacy (EF) code will still see the originals, e.g. KeyAttribute and then follow the EF route, whereas the EFC code won't see the attributes because they were redefined. 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.
What drives me nuts is that no matter what I do, EFC manages to "sneak in" shadow properties and similar crappy things. This gets to the point that I really want to turn off ALL EFC conventions and build the whole model by hands. After all, I am already doing that, like for the 90% of the model. I would rather want EFC throw (with a meaningful error message) than silently do anything that I don't expect it to do.
Following the advice of @IvanStoev here is what I currently have:
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.
So, when I stick in this CreateModel into 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 e.g. 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. 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.
Any further ideas will be highly appreciated. Thanks a lot!