Entity Framework adds redundant duplicate discriminator column to table

1k views Asked by At

I've been searching already for hours and I can't seem to find the issue.

I am building an Entity Framework Fluent Api Code First TPH app. When I Add-Migration EF add's my "Type" column just fine, but it also adds a redundant Discriminator column (it should be overwritten by "Type"). I Use Map to specify the Type column name and possible values, this approach seems to work just fine for most of the domain models but this one gets a redundant second discriminator column and I can't seem to find the reason. Bond inherits from Asset in the domain model.

Heres my code:

public class BondConfiguration : EntityTypeConfiguration<Bond>
{
    public BondConfiguration()
    {
        Property(b => b.IssueDate)
            .HasColumnName("BondIssueDate")
            .HasColumnType(DatabaseVendorTypes.TimestampField)
            .IsRequired();

        Property(b => b.MaturityDate)
            .HasColumnName("BondMaturityDate")
            .HasColumnType(DatabaseVendorTypes.TimestampField)
            .IsRequired();

        HasRequired(b => b.Currency).WithRequiredDependent();

        Property(b => b.Coupon.Rate);

        Property(b => b.Coupon.Amount);

        Property(b => b.FaceValue)
            .HasColumnName("BondFaceValue")
            .IsRequired();
    }
}

public class AssetConfiguration : EntityTypeConfiguration<Asset>
{
    public AssetConfiguration()
    {
        Property(a => a.IsDeleted).HasColumnName("IsDeleted");

        HasKey(a => a.Id);

        ToTable("tbl_Asset");

        Property(a => a.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
            .HasColumnName("AssetId");

        Property(a => a.Name)
            .HasColumnName("AssetName")
            .IsRequired();

        Property(a => a.Isin)
            .HasColumnName("AssetISIN");

        Map<Bond>(p => p.Requires("AssetClass").HasValue((int)AssetClass.Bond));
    }
}

Domain Model:

public class Bond : Asset
{
    public DateTime IssueDate { get; set; }

    public DateTime MaturityDate { get; set; }

    public BondCoupon Coupon { get; set; }

    public Currency Currency { get; set; }

    public decimal FaceValue { get; set; }

    public IEnumerable<ValidationRule> SetCoupon(decimal amount, decimal rate)
    {
        var newCoupon = new BondCoupon
        {
            Rate = rate,
            Amount = amount
        };

        if (Validate(new SetBondCouponValidator(newCoupon),out IEnumerable<ValidationRule> brokenRules))
        {
            Coupon = new BondCoupon
            {
                Rate = rate,
                Amount = amount
            };
        }
        return brokenRules;
    }
}

public abstract class BaseAsset<T> : BaseEntity<T> where T : BaseEntity<T>, new()
{
    public string Name { get; set; }

    public string Isin { get; set; }
}

public class Asset : BaseAsset<Asset>, IEntityRoot
{
}

public class BaseEntity<T> where T : BaseEntity<T>, new()
{
    public int Id { get; set; }

    public bool IsDeleted { get; set; }

    public bool Validate(IValidator validator, out IEnumerable<ValidationRule> brokenRules)
    {
        brokenRules = validator.GetBrokenRules();
        return validator.IsValid();
    }
}
2

There are 2 answers

1
Ivan Stoev On BEST ANSWER

You must be very careful when using any of EF6 inheritance. EF uses reflection to discover all classes in the same assembly which directly or indirectly inherit some of the entities which are part of EF inheritance and considers them being a part of the entity hierarchy, even if they are not used/referenced/configured anywhere from the EF model.

So just adding another class (in your real case it's called Equity)

public Asset2 : Asset { }

is enough to introduce the standard Discriminator column because it's not configured to use the discriminator column setup for the Bond class.

This behavior is source of unexpected errors like yours and has been changed in EF Core where only the explicitly configured derived classes are considered.

In EF6, either mark such classes with NotMapped attribute, use Ignore fluent API or properly map them as entity.

0
David Browne - Microsoft On

Here's a complete non-repro. Ensure that your EntityTypeConfigurations are wired-up in OnModelCreating, and are actually running when the model is initialized. Also don't name tables with a "tbl_" prefix.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Data.SqlClient;
using System.Linq;

namespace ConsoleApp8
{

    public class Bond : Asset
    {
        public DateTime IssueDate { get; set; }

        public DateTime MaturityDate { get; set; }

        //public BondCoupon Coupon { get; set; }

        //public Currency Currency { get; set; }

        public decimal FaceValue { get; set; }


    }

    public abstract class BaseAsset<T> : BaseEntity<T> where T :  new()
    {
        public string Name { get; set; }

        public string Isin { get; set; }
    }

    public class Asset : BaseAsset<Asset>
    {
    }

    public class BaseEntity<T> where T :  new()
    {
        public int Id { get; set; }

        public bool IsDeleted { get; set; }


    }
    public class BondConfiguration : EntityTypeConfiguration<Bond>
    {
        public BondConfiguration()
        {

            Property(b => b.FaceValue)
                .HasColumnName("BondFaceValue")
                .IsRequired();
        }
    }
    public  enum  AssetClass
    {
        Bond = 1
    }
    public class AssetConfiguration : EntityTypeConfiguration<Asset>
    {
        public AssetConfiguration()
        {
            Property(a => a.IsDeleted).HasColumnName("IsDeleted");

            HasKey(a => a.Id);

            ToTable("Asset");

            Property(a => a.Id)
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
                .HasColumnName("AssetId");

            Property(a => a.Name)
                .HasColumnName("AssetName")
                .IsRequired();

            Property(a => a.Isin)
                .HasColumnName("AssetISIN");

            Map<Bond>(p => p.Requires("AssetClass").HasValue((int)AssetClass.Bond));
        }
    }

    class Db : DbContext
    {
        public DbSet<Bond> Bonds { get; set; }
        public DbSet<Asset> Assets { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new AssetConfiguration());
            modelBuilder.Configurations.Add(new BondConfiguration());
        }
    }



    class Program
    {      

        static void Main(string[] args)
        {

            Database.SetInitializer(new DropCreateDatabaseAlways<Db>());

            using (var db = new Db())
            {
                db.Database.Log = m => Console.WriteLine(m);

                db.Database.Initialize(true);






            }


            Console.WriteLine("Hit any key to exit");
            Console.ReadKey();


        }
    }
}

outputs (in part):

CREATE TABLE [dbo].[Asset] (
    [AssetId] [int] NOT NULL IDENTITY,
    [AssetName] [nvarchar](max) NOT NULL,
    [AssetISIN] [nvarchar](max),
    [IsDeleted] [bit] NOT NULL,
    [IssueDate] [datetime],
    [MaturityDate] [datetime],
    [BondFaceValue] [decimal](18, 2),
    [AssetClass] [int],
    CONSTRAINT [PK_dbo.Asset] PRIMARY KEY ([AssetId])
)