Why does ModelBuilder.Entity
Can HasData be enhanced to accept a derived type?
{System.InvalidOperationException: The seed entity for entity type 'Blog' cannot be added because the value provided is of a derived type 'RssBlog'. Add the derived seed entities to the corresponding entity type.
at Microsoft.EntityFrameworkCore.Metadata.Internal.EntityType.AddData(Object[] data)
at Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder.HasData(Object[] data)
at Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.HasData(TEntity[] data)
at Context.<>c.<OnModelCreating>b__5_0(EntityTypeBuilder`1 builder) in
at Microsoft.EntityFrameworkCore.ModelBuilder.Entity[TEntity](Action`1 buildAction)
at Context.OnModelCreating(ModelBuilder modelBuilder) in
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)}
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
class Program
{
static void Main(string[] args)
{
var connection = new SqliteConnection("datasource=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<Context>()
.UseSqlite(connection)
.Options;
using (var ctx = new Context(options))
{
ctx.Database.EnsureCreated();
}
connection.Close();
}
}
public class Context : DbContext
{
public Context(DbContextOptions options) : base(options) { }
public DbSet<Blog> Blogs { get; private set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<RssBlog>().HasBaseType<Blog>();
modelBuilder.Entity<Blog>(builder =>
{
builder.HasData(new RssBlog());
});
}
}
public abstract class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
}
public class RssBlog : Blog
{
public string RssUrl { get; set; }
}
EF Core version: 2.1.1
Database Provider: Microsoft.EntityFrameworkCore.Sqlite
Operating system: win7
IDE: Visual Studio 2017 15.4
@gojanpaolo Do you expect a RssBlog or a Blog row to be inserted?
@ajcvickers I expect a Blog row to be inserted.
Db will only have Blog table with Discriminator column.
@gojanpaolo Let me ask that differently. Do you expect a row with the discriminator set to RssBlog and the RssUrl column populated with whatever value it has?
@ajcvickers Yes. :)
Putting this on the backlog for now. We would be interesting in hearing from any others who hit this issue.
Thank you for considering. :)
Shouldn't the DbSet
public DbSet<RssBlog> Blogs { get; private set; }
rather than
public DbSet<Blog> Blogs { get; private set; }
@GZidar We can expose only the base type DbSet<Blog> by explicitly configuring the model builder modelBuilder.Entity<RssBlog>().HasBaseType<Blog>();
See also: https://docs.microsoft.com/en-us/ef/core/modeling/inheritance#conventions
If you don't want to expose a DbSet for one or more entities in the hierarchy, you can use the Fluent API to ensure they are included in the model. And if you don't rely on conventions you can specify the base type explicitly using
HasBaseType.
@gojanpaolo Thanks for the clarification. :-)
I hit the issue when trying to apply Enumeration pattern and seed base type with derived types (derived types add no new properties into class signature, just logical separation).
See CardType implementation in eShopOnContainers repository for example.
So, trying to seed like this will fail (because instead of CardType items it returns specific derived type for each item):
builder.Entity<CardType>().HasData(Enumeration.GetAll<CardType>());
It there any recommended workaround for situation, when derived class used in seeding is not presented in model?
@dmitryshunkov Here's a similar workaround I did on my project.
```c#
public MyContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new CardTypeConfiguration()).HasAllCardTypes();
}
}
internal sealed class CardTypeConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder
{
var discriminatorBuilder = builder.HasDiscriminator(c => c.Name);
foreach (var cardType in Enumeration.GetAll<CardType>())
discriminatorBuilder.HasValue(cardType.GetType(), cardType.Name);
}
}
internal static class ModelBuilderCardTypeConfigurationExtensions
{
///
/// Configures HasData for all CardType.
///
///
/// Workaround until ef core support
/// ModelBuilder.Entity
/// https://github.com/aspnet/EntityFrameworkCore/issues/12841
///
public static void HasAllCardTypes(this ModelBuilder modelBuilder)
{
foreach (var cardType in Enumeration.GetAll
modelBuilder.Entity(cardType.GetType())
.HasData(cardType);
}
}
```
@gojanpaolo I guess, your situation is a bit different, as your derived type (RssBlog) is presented in the model (with discriminator). And what I'm trying to achieve is to have CardType table with "Id", "Name" columns, but seed it with derived types instances (no additional properties, so I don't need TPH for CardType hierarchy).
For now, as a workaround, I just manually construct anonymous type:
builder.Entity<CardType>().HasData(Enumeration.GetAll<CardType>().Select(p => new { p.Id, p.Name })
I simplified the example, in reality there are some other columns.
Discussed again in triage and decided that for now at least we will not change the behavior here.
Most helpful comment
Putting this on the backlog for now. We would be interesting in hearing from any others who hit this issue.