0

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();
    }
}
qubits
  • 1,227
  • 3
  • 20
  • 50
  • Could you add the `Asset` and `Bond` classes so we can reproduce the issue? – Ivan Stoev Sep 09 '17 at 11:56
  • I just added the domain model. Thanks for looking at this. – qubits Sep 09 '17 at 12:12
  • Can't reproduce with the provided model. Do you have any other class in the same project (assembly) which inherits `Asset` or `Bond`? – Ivan Stoev Sep 09 '17 at 12:28
  • The entire code base is hosted on Github here: https://github.com/piotr-mamenas/performance-app – qubits Sep 09 '17 at 12:30
  • I see `Equity` class there inheriting `Asset`. You may think it's not discoverable (because there are no explicit reference/db set/entity configuration), but in fact EF scans the assembly containing the TPH base entity for classes which directly or indirectly inherit from it. Mark it as [NotMapped] until you decide how are you going to map it to the existing hierarchy. – Ivan Stoev Sep 09 '17 at 12:43

2 Answers2

3

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.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • 1
    Ok so this is a bug in EF6 which was fixed in Core? Thank you, I adjusted the model and included equity configuration, afterwards I regenerated the migration. The discriminator column is removed in the migration. Problem solved. – qubits Sep 09 '17 at 13:11
1

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])
)
David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67