Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fluent Api triggers table creation of base classes #10417

Closed
JimBobSquarePants opened this issue Nov 28, 2017 · 6 comments
Closed

Fluent Api triggers table creation of base classes #10417

JimBobSquarePants opened this issue Nov 28, 2017 · 6 comments
Labels
closed-no-further-action The issue is closed and no further action is planned.

Comments

@JimBobSquarePants
Copy link

JimBobSquarePants commented Nov 28, 2017

If I attempt to configure primary key behaviour against base classes using the fluent API only those classes are created against the database. This behaviour is contrary to my understanding that abstract classes do not trigger table behaviour.

Steps to reproduce

Given the following base classes

public abstract class Entity : IEntity<Guid>
{
    public Guid Id { get; set; }
}

public abstract class RangeEntity : IRangeEntity<int, Guid>
{
    public Guid Id { get; set; }

    public int RangeId { get; set; }
}

And configuration code to normalize behaviour across my concrete types

const string SequentialGuid = "newsequentialid()";

// Configure the base entities to use sequential guid's as primary keys
builder.Entity<Entity>(b =>
{
    b.HasKey(p => p.Id);
    b.Property(p => p.Id).HasDefaultValueSql(SequentialGuid);
});

// Configure sequential guid primary key and additional incremental key
builder.Entity<RangeEntity>(b =>
{
    b.HasKey(p => p.Id);
    b.Property(p => p.Id).HasDefaultValueSql(SequentialGuid);
    b.HasAlternateKey(p => p.RangeId);
});

The library will create two tables in my database Entity and RangeEntity. These tables contain some of the properties of inheriting classes and foreign keys for inheriting classes also. No other tables are generated. I have configured DbSet roperties for all my concrete types.

I would expect EF Core to create tables and implement those rules for all inheriting concrete types and not create tables for the base types.

Further technical details

EF Core version:

  • Microsoft.AspNetCore.Identity.EntityFrameworkCore Version 2.0.1
  • Microsoft.EntityFrameworkCore.SqlServer Version 2.0.1
  • Microsoft.EntityFrameworkCore.Tools Version 2.0.1
    Database Provider: Microsoft.EntityFrameworkCore.SqlServer
    Operating system: Windows 10
    IDE: Visual Studio 2017
@ajcvickers
Copy link
Member

@JimBobSquarePants Using the Entity call tells EF to map the entity type, which in turn causes it to become the base type of a TPH mapping. If you want the type instead to be an un-mapped base type, then don't explicitly configure it as as an entity type with a call to Entity or a by having DbSet for it on the context.

Something like this can be used to bulk-configure all entity types that inherit from a type:

foreach (var entityType in modelBuilder.Model.GetEntityTypes()
    .Where(e => typeof(Entity).IsAssignableFrom(e.ClrType)))
{
    modelBuilder
        .Entity(entityType.ClrType)
        .Property(nameof(Entity.Id))
        .HasDefaultValueSql(SequentialGuid);
}

Issues #9117 and #6787 are about making this somewhat easier.

Also note that Id will be mapped as the key by convention, so it doesn't need to be mapped explicitly. HasAlternateKey is unlikely to be useful--see discussion here: #8645

Finally, I'm curious where your "understanding that abstract classes do not trigger table behaviour" comes from? I'm asking in case we need to update docs anywhere.

@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label Nov 28, 2017
@ghost
Copy link

ghost commented Jan 21, 2018

Edit : After applying a migration, your code works partially. However, it creates static default values in the
database. What I wanted was being able to provide default values for those fields on the run. I realized what I want is called TPC, and it is not available in EF Core yet. Please correct me if I am wrong.

I have a similar problem regarding this issue. I have a base type that will not be mapped seperately to the database, but which contains some fields that I want to be present in all of my entities. And I want EF to automatically populate those fields.

Here is the base class that I have :

public abstract class EntityBase
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime CreatedAt { get; set; }
    public int CreatedBy { get; set; }
}

Here is one of my entities that will be mapped to database :

public class Bill : EntityBase
{
    public DateTime DateTime { get; set; }
    public double Total { get; set; }
}

I have tried the following to populate fields, but it fails stating EntityBase is not present in database.

modelBuilder.Entity<EntityBase>()
            .Property(x => x.CreatedBy)
            .ValueGeneratedOnAdd()
            .HasDefaultValue(UserId);

From your answer, I have tried the following. It works without an error, but the field is not populated in the database (It still has the default int value 0).

foreach (var entityType in modelBuilder.Model.GetEntityTypes()
               .Where(e => typeof(EntityBase).IsAssignableFrom(e.ClrType)))
           {
               modelBuilder
                   .Entity(entityType.ClrType)
                   .Property(nameof(EntityBase.CreatedBy))
                   .ValueGeneratedOnAdd()
                   .HasDefaultValue(5);
           }

I am using :
.Net Core 2.0
EF Core 2.0
Npgsql.EntityFrameworkCore.PostgreSQL 2.0
Windows 10

@ajcvickers
Copy link
Member

@akyildizfirat I ran your code and it works fine for me. Can you please post a new issue with a runnable project/solution or code listing that demonstrates what you are seeing?

Here's my little test listing using your code for reference:

public abstract class EntityBase
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime CreatedAt { get; set; }
    public int CreatedBy { get; set; }
}

public class Bill : EntityBase
{
    public DateTime DateTime { get; set; }
    public double Total { get; set; }
}

public class TestDbContext : DbContext
{
    public DbSet<Bill> Bills { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes()
            .Where(e => typeof(EntityBase).IsAssignableFrom(e.ClrType)))
        {
            modelBuilder
                .Entity(entityType.ClrType)
                .Property(nameof(EntityBase.CreatedBy))
                .ValueGeneratedOnAdd()
                .HasDefaultValue(5);
        }
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");
}

public class Program
{
    public static void Main()
    {
        using (var context = new TestDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.Add(new Bill());

            context.SaveChanges();
        }

        using (var context = new TestDbContext())
        {
            Console.WriteLine(context.Bills.Single().CreatedBy);
        }
    }
}

@ghost
Copy link

ghost commented Jan 26, 2018

I am sorry for the late reply and the confusion. I should have made a new post without editing my previous one. Let me try to be more specific.

I have an abstract base class and many models inheriting from it. In the database, I want to have tables for concrete types. In addition, I am trying to make EF automatically populate the fields of the base class for each model for any query that I execute, if that is possible.

public abstract class EntityBase
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime CreatedAt { get; set; }
    public int CreatedBy { get; set; }
}

public class Bill : EntityBase
{
    public DateTime DateTime { get; set; }
    public double Total { get; set; }
}

public class TestDbContext : DbContext
{
    public DbSet<Bill> Bills { get; set; }
    public int UserId { get; set; }

    public TestDbContext()
    {
        UserId = GetUserIdFromSomewhere();
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes()
            .Where(e => typeof(EntityBase).IsAssignableFrom(e.ClrType)))
        {
            modelBuilder
                .Entity(entityType.ClrType)
                .Property(nameof(EntityBase.CreatedBy))
                .ValueGeneratedOnAdd()
                .HasDefaultValue(UserId);
        }
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");
}

To be clear, I want a different UserId value inserted on each query at the runtime, depending on the value I get at the DbContext's constructor.
Is there a possible way to achieve that without writing configuration code for every model ? I am trying to write one piece of code for EntityBase, so that each model inheriting from it gets its fields populated by EF dynamically at runtime.

Thank you.

@ajcvickers
Copy link
Member

@akyildizfirat There isn't anything built-in to do that. I suspect you will be able to do with state-changing events, which is being tracked by issue #626

@ghost
Copy link

ghost commented Jan 29, 2018

Thank you very much for your answers.

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-no-further-action The issue is closed and no further action is planned.
Projects
None yet
Development

No branches or pull requests

2 participants