dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.18k forks source link

Non-empty second migration in TPC scheme using inheritance #30058

Closed amyboose closed 1 year ago

amyboose commented 1 year ago

I'm using a bit change example from microsoft documentation "What's new in EF 7":

using Microsoft.EntityFrameworkCore;
using System.Reflection;

namespace EfCoreTpc;
public class Program
{
    public static void Main(params string[] args)
    {
        IHost host = Host.CreateDefaultBuilder()
        .ConfigureServices(services =>
        {
            services.AddDbContext<MyContext>(builder =>
            {
                builder.UseNpgsql("Host=localhost;Port=7435;Database=testdb;Username=admin;Password=testpass");
            });
        })
        .Build();

        host.Run();
    }
}

public abstract class Animal
{
    protected Animal()
    {

    }

    protected Animal(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string Species { get; }

    public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
    protected Pet(string name)
        : base(name)
    {
    }

    public string? Vet { get; set; }

    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
    protected FarmAnimal()
        : base()
    {

    }

    public FarmAnimal(string name, string species)
        : base(name)
    {
        Species = species;
    }

    public override string Species { get; }

    [Precision(18, 2)]
    public decimal Value { get; set; }

    public override string ToString()
        => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
    public Cat(string name, string educationLevel)
        : base(name)
    {
        EducationLevel = educationLevel;
    }

    public string EducationLevel { get; set; }
    public override string Species => "Felis catus";

    public override string ToString()
        => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
    public Dog(string name, string favoriteToy)
        : base(name)
    {
        FavoriteToy = favoriteToy;
    }

    public string FavoriteToy { get; set; }
    public override string Species => "Canis familiaris";

    public override string ToString()
        => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
    public Human(string name)
        : base(name)
    {
    }

    public override string Species => "Homo sapiens";

    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();

    public override string ToString()
        => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
           $" eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Food
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class MyContext : DbContext
{
    public MyContext(DbContextOptions options)
        : base(options)
    {

    }

    public DbSet<Animal> Animals => Set<Animal>();
    public DbSet<Pet> Pets => Set<Pet>();
    public DbSet<FarmAnimal> FarmAnimals => Set<FarmAnimal>();
    public DbSet<Cat> Cats => Set<Cat>();
    public DbSet<Dog> Dogs => Set<Dog>();
    public DbSet<Human> Humans => Set<Human>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

        modelBuilder.Entity<Animal>(builder =>
        {
            builder
                .Property(p => p.Id)
                .UseHiLo();

            builder
                .Ignore(p => p.Food);

        });

        modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
    }
}

Then I create 2 migrations using: 1) command "add-migration Init" 2) command "add-migration t2" (immediatly after first migration)

The second migration must be empty. But I've got non-empty result:

using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EfCoreTpc.Migrations
{
    /// <inheritdoc />
    public partial class t2 : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Pet");
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Pet",
                columns: table => new
                {
                    Id = table.Column<int>(type: "integer", nullable: false),
                    Name = table.Column<string>(type: "text", nullable: false),
                    Vet = table.Column<string>(type: "text", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Pet", x => x.Id);
                });
        }
    }
}

Provider and version information

EF Core version: 7.0.1 Database provider: Npgsql Entity Framework Core provider for PostgreSQL Target framework: NET 7.0 Operating system: Windows 10 IDE: Visual Studio 2022 17.4

ajcvickers commented 1 year ago

Note for triage: repros with SQL Server and on latest daily. Minimal repro below.

public class Program
{
    public static void Main(params string[] args)
    {
    }
}

public abstract class Animal
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public abstract class Pet : Animal
{
    public string? Vet { get; set; }
    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class Cat : Pet
{
    public string EducationLevel { get; set; }
}

public class Dog : Pet
{
    public string FavoriteToy { get; set; }
}

public class Human : Animal
{
    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();
}

public class MyContext : DbContext
{
    public DbSet<Animal> Animals => Set<Animal>();
    public DbSet<Pet> Pets => Set<Pet>();
    public DbSet<Cat> Cats => Set<Cat>();
    public DbSet<Dog> Dogs => Set<Dog>();
    public DbSet<Human> Humans => Set<Human>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(@"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();
    }
}