Closed dzsibi closed 10 months ago
I've found this happening to several of our code bases, this worked in EF6 and EF7. It's trivial to reproduce having an entity like this:
class Entity {
public int Id { get; set; }
public Entity Parent { get; set; }
public int? ParentId { get; set; }
public ICollection<Entity> Children { get; set; } = new HashSet<Entity>();
}
/cc @AndriySvyryd
Same case here. The issue is preventing us from upgrading to .Net 8.
I just tried debugging this in EF 7. It turns out the only reason it works is because Property.GetValueConverter()
isn't throwing but is silently giving up and returning null
after 10_000 tries.
The first part was a deliberate breaking change, the second part (when generating the next migration) wasn't
@AndriySvyryd It seems that the current algorithm breaks the loop when we get back to the property we've originally started with or the next property would be the same as the current one. Wouldn't it be better to maintain a set of the properties we've already traversed and break when we get to a property already in the set?
Here you can see exactly what I mean: https://github.com/RenesansJG/efcore/commit/e531d9d055bd787005ea0709bfa7bc0623509bcd
I tried it out and it seemed to solve the problem presented in the original post. I know the extra allocation wouldn't do any good for performance but it can be optimized so that the allocation only gets done if it's actually needed.
Wouldn't it be better to maintain a set of the properties we've already traversed and break when we get to a property already in the set?
It would be more accurate, but would have a measurable negative perf impact. We can improve the cycle breaking logic without tracking more than 1 property.
This issue also surfaces during scaffolding.
I try to scaffold an Oracle database created with EF (not-core) 6. Scaffolding works with Microsoft.EntityFrameworkCore.Relational 7.0.14 and Oracle.EntityFrameworkCore 7.21.12, but fails with Microsoft.EntityFrameworkCore.Relational 8.0.0 and Oracle.EntityFrameworkCore 8.21.121.
This is quite inconvenient, because there is not much I can do about the existing database structure (which works fine, by the way). So this new behavior de facto breaks scaffolding for my database.
When I create a DbContext with EF 7 and then upgrade to EF 8 I get a runtime error, but at least I can now try to adapt the structure to get a consistent EF 8 model.
@Peter-B- I think this will be fixed in 8.0.2 - and how is it "failing"?
Hi @ErikEJ,
When I run dotnet ef DbContext scaffold ...
it fails with this error message:
System.InvalidOperationException: A relationship cycle involving the property 'PcbType (Dictionary<string, object>).DutCoordinateSetId' was detected. This prevents Entity Framework from determining the correct configuration. Review the foreign keys defined on the property and the corresponding principal property and either remove one of them or specify 'ValueConverter' explicitly on one of the properties.
at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.GetValueConverter()
at Microsoft.EntityFrameworkCore.Storage.TypeMappingInfo..ctor(IReadOnlyList`1 principals, Nullable`1 fallbackUnicode, Nullable`1 fallbackSize, Nullable`1 fallbackPrecision, Nullable`1 fallbackScale)
at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMappingInfo..ctor(IReadOnlyList`1 principals, String storeTypeName, String storeTypeNameBase, Nullable`1 fallbackUnicode, Nullable`1 fallbackFixedLength, Nullable`1 fallbackSize, Nullable`1 fallbackPrecision, Nullable`1 fallbackScale)
at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMappingSource.FindMapping(IProperty property)
at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.<>c.<get_TypeMapping>b__91_0(IProperty property)
at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory)
at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.get_TypeMapping()
at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty.FindTypeMapping()
at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.FindRelationalTypeMapping(IReadOnlyProperty property)
at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.GetColumnType(IReadOnlyProperty property)
at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.GetColumnType(IProperty property)
at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.AddDefaultMappings(RelationalModel databaseModel, IEntityType entityType, IRelationalTypeMappingSource relationalTypeMappingSource)
at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.Create(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, Boolean designTime)
at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.Add(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, Boolean designTime)
at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelRuntimeInitializer.InitializeModel(IModel model, Boolean designTime, Boolean prevalidation)
at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
at Microsoft.EntityFrameworkCore.Scaffolding.Internal.RelationalScaffoldingModelFactory.Create(DatabaseModel databaseModel, ModelReverseEngineerOptions options)
at Microsoft.EntityFrameworkCore.Scaffolding.Internal.ReverseEngineerScaffolder.ScaffoldModel(String connectionString, DatabaseModelFactoryOptions databaseOptions, ModelReverseEngineerOptions modelOptions, ModelCodeGenerationOptions codeOptions)
at Microsoft.EntityFrameworkCore.Design.Internal.DatabaseOperations.ScaffoldContext(String provider, String connectionString, String outputDir, String outputContextDir, String dbContextClassName, IEnumerable`1 schemas, IEnumerable`1 tables, String modelNamespace, String contextNamespace, Boolean useDataAnnotations, Boolean overwriteFiles, Boolean useDatabaseNames, Boolean suppressOnConfiguring, Boolean noPluralize)
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.ScaffoldContextImpl(String provider, String connectionString, String outputDir, String outputDbContextDir, String dbContextClassName, IEnumerable`1 schemaFilters, IEnumerable`1 tableFilters, String modelNamespace, String contextNamespace, Boolean useDataAnnotations, Boolean overwriteFiles, Boolean useDatabaseNames, Boolean suppressOnConfiguring, Boolean noPluralize)
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.ScaffoldContext.<>c__DisplayClass0_0.<.ctor>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
No models or db context are created.
I saw that there are PRs for 8.0.2. I just wanted to bring to your attention, that this also affects scaffolding.
Thanks for your prompt reply and the fast fix Peter
Was this addressed in v8.0.201? I'm still getting this error with SDK 8.0.201 installed. I wanted to double-check before creating a sample to reproduce the issue.
Edit: scratch that! Upgrading the SDK to 8.0.201 did not work, but — maybe obviously? — updating EF NuGet packages to 8.0.2 did work.
Hello,
Take the following
DbContext
:When I want to add a migration using
dotnet ef migrations add Init --verbose
, it works fine. Let's add another entity:A new migration can no longer be added, with the following error message:
The new entity did not introduce any new relationship cycles, yet EF was able to handle the previous one just fine. So I started digging. The exception is thrown here:
https://github.com/dotnet/efcore/blob/45673126512a0fe99ef73bf5c1e5701012fd9c26/src/EFCore/Metadata/Internal/Property.cs#L812C22-L812C22
There is this condition:
In the first case, on the second cycle, this condition is triggered with
principalProperty == this
, and the function returns. In the second case, with the additional entity, this does not happen, and an exception is thrown. I am not sure why this behaves like this. If the goal was to detect cycles and throw, it fails to throw on the trivial cycle in the first example. If the goal was to break cycles, it throws anyways if there is a "leaf", as in a related entity that is not included in the cycle.So let's try following the recommendation in the exception, and add value converters to the properties involved:
This works, and the migration is generated as expected. We are not out of the woods yet, however, as
dotnet ef database update --verbose
will fail with the following error:Looks familiar, but the stack is very different. It turns out that the generated migration conveniently forgot about the value converters:
And when this is loaded, the annotation is once again missing. We could work around this by overriding the implementation of AnnotationCodeGenerator:
This will include a null-valued annotation in the generated code for ValueConverter and ProviderClrType, and the migration will run:
Once again, I don't know whether this is intentional or not. The recommended workaround does not survive code generation, and overriding methods in AnnotationCodeGenerator seems hacky at best. To me, this whole thing smells like a bug somewhere, but I am not familiar enough with the codebase to pinpoint exactly where.
Provider and version information
EF Core version: 8.0 Database provider: Npgsql.EntityFrameworkCore.PostgreSQL Target framework: .NET 8.0 Operating system: Ubuntu 22.04 IDE: JetBrains Rider 2023.2.3