efcore icon indicating copy to clipboard operation
efcore copied to clipboard

FK for generated key using TPC sometimes gets temporary value persisted to database on SaveChanges

Open ajcvickers opened this issue 3 years ago • 0 comments

using (var context = new AnimalsContext())
{
    await context.Database.EnsureDeletedAsync();

    Console.WriteLine(context.Model.ToDebugString());
    Console.WriteLine();

    await context.Database.EnsureCreatedAsync();

    var catFood = new PetFood("Lily's Kitchen", LifeStage.Adult);
    var dogFood = new PetFood("Canagan", LifeStage.Adult);
    var hay = new FarmFood("Hay");
    var sushi = new HumanFood("Sushi", 670);

    var arthur = new Human("Arthur") { Food = sushi };
    var wendy = new Human("Wendy");
    var christi = new Human("Christi");

    var alice = new Cat("Alice", "MBA") { Vet = "Pengelly", Food = catFood, Humans = { arthur, wendy } };

    var mac = new Cat("Mac", "Preschool") { Vet = "Pengelly", Food = catFood, Humans = { arthur, wendy } };

    var toast = new Dog("Toast", "Mr. Squirrel") { Vet = "Pengelly", Food = dogFood, Humans = { arthur, wendy } };

    var clyde = new FarmAnimal("Clyde", "Equus africanus asinus") { Value = 100.0m, Food = hay };

    wendy.FavoriteAnimal = toast;
    arthur.FavoriteAnimal = alice;
    christi.FavoriteAnimal = clyde;

    await context.AddRangeAsync(wendy, arthur, christi, alice, mac, toast, clyde);
    await context.SaveChangesAsync();
}

Console.WriteLine();

using (var context = new AnimalsContext())
{
    foreach (var human in context.Humans)
    {
        Console.WriteLine($"Human.FavoriteAnimalId = {context.Entry(human).Property("FavoriteAnimalId").CurrentValue}");
    }
}

public abstract class 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
{
    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 abstract class Food
{
    public Guid Id { get; set; }
}

public class PetFood : Food
{
    public PetFood(string brand, LifeStage lifeStage)
    {
        Brand = brand;
        LifeStage = lifeStage;
    }

    public string Brand { get; set; }
    public LifeStage LifeStage { get; set; }

    public override string ToString()
        => $"Pet food by '{Brand}' ({Id}) for life stage {LifeStage}";
}

public enum LifeStage
{
    Juvenile,
    Adult,
    Senior
}

public class HumanFood : Food
{
    public HumanFood(string name, int calories)
    {
        Name = name;
        Calories = calories;
    }

    [Column("Name")]
    public string Name { get; set; }

    public int Calories { get; set; }

    public override string ToString()
        => $"{Name} ({Id}) with calories {Calories}";
}

public class FarmFood : Food
{
    public FarmFood(string name)
    {
        Name = name;
    }

    [Column("Name")]
    public string Name { get; set; }

    public override string ToString()
        => $"{Name} ({Id})";
}

public class AnimalsContext : DbContext
{
    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>();

    public DbSet<Food> Foods
        => Set<Food>();

    public DbSet<PetFood> PetFoods
        => Set<PetFood>();

    public DbSet<FarmFood> FarmFoods
        => Set<FarmFood>();

    public DbSet<HumanFood> HumanFoods
        => Set<HumanFood>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .EnableSensitiveDataLogging()
            .UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}");

        optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
    }

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

        modelBuilder.Entity<FarmAnimal>().Property(e => e.Species);

        modelBuilder.Entity<Human>()
            .HasMany(e => e.Pets)
            .WithMany(e => e.Humans)
            .UsingEntity<Dictionary<object, string>>(
                "PetsHumans",
                r => r.HasOne<Pet>().WithMany().OnDelete(DeleteBehavior.Cascade),
                l => l.HasOne<Human>().WithMany().OnDelete(DeleteBehavior.ClientCascade));
    }
}

Human.FavoriteAnimalId = -2147482646
Human.FavoriteAnimalId = -2147482644
Human.FavoriteAnimalId = -2147482642

ajcvickers avatar Aug 10 '22 12:08 ajcvickers