diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsAssembly.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsAssembly.cs index 277c0ca71a2..e6c5ce2036e 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsAssembly.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsAssembly.cs @@ -43,6 +43,27 @@ public MigrationsAssembly( _logger = logger; } + private static Type? GetDbContextType(TypeInfo typeInfo) + { + // Walk the inheritance chain to find the first DbContextAttribute. + // This supports inheritance while avoiding AmbiguousMatchException + // that would occur with GetCustomAttribute(inherit: true) when multiple + // attributes exist in the hierarchy. + var currentType = typeInfo.AsType(); + while (currentType != null && currentType != typeof(object)) + { + var attribute = currentType.GetCustomAttribute(inherit: false); + if (attribute != null) + { + return attribute.ContextType; + } + + currentType = currentType.BaseType; + } + + return null; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -60,7 +81,7 @@ IReadOnlyDictionary Create() var items = from t in Assembly.GetConstructibleTypes() where t.IsSubclassOf(typeof(Migration)) - && t.GetCustomAttribute(inherit: false)?.ContextType == _contextType + && GetDbContextType(t) == _contextType let id = t.GetCustomAttribute()?.Id orderby id select (id, t); @@ -94,7 +115,7 @@ public virtual ModelSnapshot? ModelSnapshot => _modelSnapshot ??= (from t in Assembly.GetConstructibleTypes() where t.IsSubclassOf(typeof(ModelSnapshot)) - && t.GetCustomAttribute(inherit: false)?.ContextType == _contextType + && GetDbContextType(t) == _contextType select (ModelSnapshot)Activator.CreateInstance(t.AsType())!) .FirstOrDefault(); diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsAssemblyTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsAssemblyTest.cs index bde37108115..ad7c5b1ee91 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsAssemblyTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsAssemblyTest.cs @@ -97,6 +97,47 @@ public void Migrations_handles_inherited_DbContextAttribute() Assert.Contains(result, t => t.Key == "20150302103200_InheritedMigration"); } + [ConditionalFact] + public void Migrations_finds_attribute_on_base_class_only() + { + var assembly = CreateMigrationsAssemblyWithAttributeOnBaseOnly(); + + // This should find the migration even though the attribute is only on the base class + var result = assembly.Migrations; + + Assert.Single(result); + Assert.Contains(result, t => t.Key == "20150302103300_DerivedMigrationWithBaseAttribute"); + } + + private IMigrationsAssembly CreateMigrationsAssemblyWithAttributeOnBaseOnly() + => new MigrationsAssembly( + new CurrentDbContext(new AttributeOnBaseContext()), + new DbContextOptions( + new Dictionary + { + { typeof(FakeRelationalOptionsExtension), new FakeRelationalOptionsExtension() } + }), + new MigrationsIdGenerator(), + new FakeDiagnosticsLogger()); + + private class AttributeOnBaseContext : DbContext; + + [DbContext(typeof(AttributeOnBaseContext))] + private class BaseMigrationWithAttribute : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + } + } + + [Migration("20150302103300_DerivedMigrationWithBaseAttribute")] + private class DerivedMigrationWithBaseAttribute : BaseMigrationWithAttribute + { + protected override void Up(MigrationBuilder migrationBuilder) + { + } + } + private IMigrationsAssembly CreateInheritedMigrationsAssembly() => new MigrationsAssembly( new CurrentDbContext(new DerivedContext()),