Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<Version>8.10.2</Version>
<Version>8.10.3</Version>
<LangVersion>13.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore;

namespace Weasel.EntityFrameworkCore.Tests.Postgresql;

public class TphBaseClassFkDbContext : DbContext
{
public TphBaseClassFkDbContext(DbContextOptions<TphBaseClassFkDbContext> options) : base(options)
{
}

public DbSet<Vehicle> Vehicles => Set<Vehicle>();
public DbSet<Truck> Trucks => Set<Truck>();
public DbSet<Sedan> Sedans => Set<Sedan>();
public DbSet<VehicleOwner> VehicleOwners => Set<VehicleOwner>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Vehicle>(entity =>
{
entity.ToTable("vehicles");
entity.HasKey(e => e.Id);
entity.HasDiscriminator<string>("Discriminator")
.HasValue<Truck>("Truck")
.HasValue<Sedan>("Sedan");
});

modelBuilder.Entity<VehicleOwner>(entity =>
{
entity.ToTable("vehicle_owners");
entity.HasKey(e => e.Id);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using JasperFx;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Weasel.Core;
using Weasel.Postgresql;
using Xunit;

namespace Weasel.EntityFrameworkCore.Tests.Postgresql;

/// <summary>
/// Reproduces GitHub issue #228: TPH topological sort fails when the base class
/// has a FK dependency, because derived types inherit the same FK causing duplicate
/// edges that inflate in-degree counts in Kahn's algorithm.
/// </summary>
public class tph_base_class_fk_end_to_end : IAsyncLifetime
{
private IHost _host = null!;

public async Task InitializeAsync()
{
_host = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddDbContext<TphBaseClassFkDbContext>(options =>
options.UseNpgsql(PostgresqlDbContext.ConnectionString));

services.AddSingleton<Migrator, PostgresqlMigrator>();
})
.Build();

await _host.StartAsync();
}

public async Task DisposeAsync()
{
await _host.StopAsync();
_host.Dispose();
}

[Fact]
public void entity_types_sorted_correctly_when_base_class_has_fk()
{
using var scope = _host.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<TphBaseClassFkDbContext>();

var entityTypes = DbContextExtensions.GetEntityTypesForMigration(context);
var names = entityTypes.Select(e => e.GetTableName()).ToList();

// vehicle_owners must come before vehicles because vehicles has a FK to vehicle_owners
var ownerIndex = names.IndexOf("vehicle_owners");
var vehicleIndex = names.IndexOf("vehicles");

ownerIndex.ShouldBeGreaterThanOrEqualTo(0, "vehicle_owners should be in the list");
vehicleIndex.ShouldBeGreaterThanOrEqualTo(0, "vehicles should be in the list");
ownerIndex.ShouldBeLessThan(vehicleIndex,
"vehicle_owners should come before vehicles due to FK dependency");
}

[Fact]
public async Task can_apply_migration_when_base_class_has_fk()
{
using var scope = _host.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<TphBaseClassFkDbContext>();

// Clean up any previous test state
await context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS vehicles");
await context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS vehicle_owners");

var migration = await _host.Services.CreateMigrationAsync(context, CancellationToken.None);
migration.ShouldNotBeNull();
migration.Migration.Difference.ShouldNotBe(SchemaPatchDifference.None);

// This would fail before the fix because the topological sort returned
// unsorted order, potentially creating vehicles before vehicle_owners
await migration.ExecuteAsync(AutoCreate.CreateOrUpdate, CancellationToken.None);

// Clean up
await context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS vehicles");
await context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS vehicle_owners");
}
}
31 changes: 31 additions & 0 deletions src/Weasel.EntityFrameworkCore.Tests/TphBaseClassFkEntities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Weasel.EntityFrameworkCore.Tests;

// TPH hierarchy where the base class has a FK to another entity.
// This reproduces GitHub issue #228: the FK on Animal is inherited by
// Cat and Dog, causing duplicate dependency edges in the topological sort
// which breaks Kahn's algorithm (in-degree inflated but only decremented once).

public abstract class Vehicle
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Discriminator { get; set; } = string.Empty;
public VehicleOwner Owner { get; set; } = default!;
}

public class Truck : Vehicle
{
public double PayloadCapacity { get; set; }
}

public class Sedan : Vehicle
{
public int PassengerCount { get; set; }
}

public class VehicleOwner
{
public Guid Id { get; set; }
public string OwnerName { get; set; } = string.Empty;
public List<Vehicle> Vehicles { get; set; } = [];
}
5 changes: 3 additions & 2 deletions src/Weasel.EntityFrameworkCore/DbContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,11 @@ internal static IReadOnlyList<IEntityType> TopologicalSortByForeignKeys(List<IEn
}

// Build adjacency: for each entity, find which other entities it depends on (via FKs)
var dependencies = new Dictionary<IEntityType, List<IEntityType>>();
// Use HashSet to avoid duplicate edges from TPH derived types inheriting the same FK
var dependencies = new Dictionary<IEntityType, HashSet<IEntityType>>();
foreach (var et in entityTypes)
{
var deps = new List<IEntityType>();
var deps = new HashSet<IEntityType>();
foreach (var fk in et.GetForeignKeys())
{
var principalKey = (fk.PrincipalEntityType.GetTableName(), fk.PrincipalEntityType.GetSchema());
Expand Down
Loading