I use EF core to create my tables (code first) and I'm encountering strange behaviour with my abstract base classes for some entities. I have the class ConfigurableDiscountas an abstract base class and multiple classes inheriting from it, for example: AfterSalesCoverage. The classes look like this:
public abstract class ConfigurableDiscount : TenantEntity, TraceChangesEntity
{
public DealType DealType { get; set; }
[Required]
[Column(TypeName = "decimal(18,4)")]
public decimal Discount { get; set; }
}
public class AfterSalesCoverage : ConfigurableDiscount
{
public Guid Id { get; set; }
[Required]
public string Name { get; set; }
}
The TenantEntity and TraceChangesEntity are another abstract class and an empty interface, respectively.
public abstract class TenantEntity : BaseDeleteEntity
{
public Guid BusinessUnitId { get; set; }
public virtual BusinessUnit BusinessUnit { get; set; }
}
public abstract class BaseDeleteEntity : BaseEntity
{
public DateTime? DeletedOn { get; set; }
}
public abstract class BaseEntity
{
[Required]
public DateTime CreatedOn { get; set; }
public Guid CreatedById { get; set; }
public virtual User CreatedBy { get; set; }
public DateTime? ChangedOn { get; set; }
public Guid? ChangedById { get; set; }
public virtual User ChangedBy { get; set; }
}
So as you can see, all of these are abstract except the AfterSalesCoverage.
If I try to build or migrate, I get the following error:
The entity type 'ConfigurableDiscount' requires a primary key to be defined.
If I do so (either defining a dummy key inside ConfigurableDiscount or shifting the Id from AfterSalesCoverage to ConfigurableDiscount) and try to migrate, I get the following error:
The filter expression 'entity => (entity.DeletedOn == null)' cannot be specified for entity type 'AfterSalesCoverage'. A filt
er may only be applied to the root entity type in a hierarchy.
Which makes me wonder why the AfterSalesCoverage isn't the root entity? The filter expression is defined/configured in the DBContext.
EDIT:
The filter in the DBContext is called inside of OnModelCreating(ModelBuilder builder) and looks like this:
private void SetSoftDeleteFilterQuery(ModelBuilder builder)
{
System.Collections.Generic.IEnumerable<Type> q = from t in Assembly.GetExecutingAssembly().GetTypes()
where t.IsClass && t.IsSubclassOf(typeof(BaseDeleteEntity))
select t;
foreach (Type type in q)
{
MethodInfo method = typeof(CPEDbContext).GetMethod("ApplySoftDeleteFilterQuery", BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo generic = method.MakeGenericMethod(type);
if (type.Name != "RequestWrapper" && type.Name != "TenantEntity" && type.Name != "BaseBenchmarkDiscount")
{
{
generic.Invoke(this, new[] { builder });
}
}
}
}
private void ApplySoftDeleteFilterQuery<Tdb>(ModelBuilder builder) where Tdb : BaseDeleteEntity
{
builder.Entity<Tdb>().HasQueryFilter(entity => entity.DeletedOn == null);
}
It seems like this filter is the center of interest in this case...
Question:
Why does the ConfigurableDiscount need a primary key? It's not supposed to be an actual entity. If a inherit directly from TenantEntity, I don't need an ID in the TenantEntity either... The ConfigurableDiscount entity is referenced nowhere but in the inheriting classes. So there is no DBSet or anything like that in my DBContext.
Related
In EF Core 3.1.15 I manage a model with a generic. I would like to store the entities in the same table basis Table-Per-Hierarchy approach (TPH pattern). Below is the model abstracted. The resulting database creates 1 table for Part and descendants with a discriminator (as expected), but instead of 1 table for BaseComputer and descendants it creates a separate table for Computers and a separate table for Laptops (not expected).
namespace EFGetStarted
{
public class BloggingContext : DbContext
{
public DbSet<Computer> Computers { get; set; }
public DbSet<Laptop> Laptops { get; set; }
public DbSet<Part> Parts { get; set; }
}
public abstract class BaseComputer<T> where T : Part
{
[Key]
public int Id { get; set; }
public List<T> Parts { get; set; }
}
public class Computer : BaseComputer<Part>
{
public string ComputerSpecificProperty { get; set; }
}
public class Laptop : BaseComputer<LaptopPart>
{
public string LaptopSpecificProperty { get; set; }
}
public class Part
{
[Key]
public int Id { get; set; }
public string PartName { get; set; }
}
public class LaptopPart : Part
{
public string LaptopSpecificPartProperty { get; set; }
}
}
I tried explicitly specifying the entity as TPH:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BaseComputer<Part>>()
.HasDiscriminator()
.HasValue<Computer>("Computer")
.HasValue<Laptop>("Laptop");
}
But this fails with the following message:
The entity type 'Laptop' cannot inherit from 'BaseComputer' because 'Laptop' is not a descendant of 'BaseComputer'.
Questions: Is it possible for me to design this model in a TPH pattern? If not, is it because "Laptop is not a descendant of BaseComputer<Part>"? And if that's the case, why is not a considered a descendant and what should I change in the class to make it a descendant?
I am trying to create a base class for all of my tables in order to make sure I have some data like CreateDate/Person, etc on all of my data.
So basically, I want my tables to inherit from a base class, but I want to have that data on each table separably (table per concrete type).
I've found some tutorials like these, but the problem with them is that I won't have any strongly named property for my tables.
weblogs.asp.net
As you see, in the link he created a dbset of the base class and there is no relation to the concrete class.
public class InheritanceMappingContext : DbContext
{
public DbSet<BillingDetail> BillingDetails { get; set; }
}
Here is my code:
public abstract class TrackableBase
{
public int Id { get; set; }
public virtual DateTime CreateDate { get; protected set; }
public TrackableBase()
{
CreateDate = DateTime.Now;
}
}
public class User : TrackableBase
{
public string Name { get; set; }
public string Email { get; set; }
}
You should add a property of type DbSet for each class and add the OnModelCreating for the mapping
public class InheritanceMappingContext : DbContext
{
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Users");
});
}
}
I have model like:
public abstract class Entity
{
public int Id { get; set; }
}
public abstract class Tree : Entity
{
public Tree() { Childs = new List<Tree>(); }
public int? ParentId { get; set; }
public string Name { get; set; }
[ForeignKey("ParentId")]
public ICollection<Tree> Childs { get; set; }
}
public abstract class Cat : Tree
{
public string ImageUrl { get; set; }
public string Description { get; set; }
public int OrderId { get; set; }
}
public class ItemCat : Cat
{
...
public virtual ICollection<Item> Items { get; set; }
}
and config classes:
public class CatConfig : EntityTypeConfiguration<Cat>
{
public CatConfig()
{
//properties
Property(rs => rs.Name).IsUnicode();
Property(rs => rs.ImageUrl).IsUnicode();
Property(rs => rs.Description).IsUnicode();
}
}
public class ItemCatConfig :EntityTypeConfiguration<ItemCat>
{
public ItemCatConfig()
{
Map(m => { m.ToTable("ItemCats"); m.MapInheritedProperties(); });
}
}
and DbContext:
public class Db : IdentityDbContext<MehaUser>
{
public Db():base("Db")
{
}
public DbSet<ItemCat> ItemCats { get; set; }
}
protected override void OnModelCreating(DbModelBuilder mb)
{
mb.Configurations.Add(new ItemCatConfig());
base.OnModelCreating(mb);
}
but get:
System.NotSupportedException: The type 'ItemCat' cannot be mapped as defined because it maps inherited properties from types that use entity splitting or another form of inheritance. Either choose a different inheritance mapping strategy so as to not map inherited properties, or change all types in the hierarchy to map inherited properties and to not use splitting
Update: I also Read this
Find the answer. just remove Map in ItemCatConfig Class.
Map(m => { m.ToTable("ItemCats"); m.MapInheritedProperties(); });
In TPC abstract classes does not implement in db.
ItemCat inherit from abstract classes and it doesn't need to Map configuration explicitly.
I have two classes that each implement an interface. One of the classes contains an ICollection of the other's interfaces.
Now I want to map this to my database using EF but get an exception (below). Is this supposed to be possible somehow?
Entity definitions for my classes (products and categories):
public interface IProduct
{
string ProductId { get; set; }
string CategoryId { get; set; }
}
public interface ICategory
{
string CategoryId { get; set; }
ICollection<IProduct> Products { get; set; };
}
public class ProductImpl : IProduct
{
public string ProductId { get; set; }
public string CategoryId { get; set; }
}
public class CategoryImpl : ICategory
{
public string CategoryId { get; set; }
public ICollection<IProduct> Products { get; set; }
}
I want to map the relationship between CategoryImpl and ProductImpl so I'm using the following OnModelCreating method in my DbContext:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var a = modelBuilder.Entity<CategoryImpl>();
a.ToTable("Categories");
a.HasKey(k => k.CategoryId);
a.Property(p => p.CategoryId);
a.HasMany(p => p.Products).WithOptional().HasForeignKey(p => p.CategoryId);
var b = modelBuilder.Entity<ProductImpl>();
b.ToTable("Products");
b.HasKey(k => k.ProductId);
b.Property(p => p.ProductId);
}
The exception I get is below. Am I supposed to somehow specify that the concrete type to be used for IProduct is ProductImpl?
System.InvalidOperationException: The navigation property 'Products'
is not a declared property on type 'CategoryImpl'. Verify that it has
not been explicitly excluded from the model and that it is a valid navigation property.
It's not possible to do it with interfaces in EF. The type of a navigation property must be mapped for the property to be mapped. And for a type to be mapped it needs to be a concrete type among other things.
If you need to have different types of products and categories you could instead use a base class for them:
public class ProductBase
{
public string ProductId { get; set; }
public string CategoryId { get; set; }
}
public class CategoryBase
{
public string CategoryId { get; set; }
public virtual ICollection<ProductBase> Products { get; set; }
}
public class DerivedProduct : ProductBase
{
}
public class DerivedCategory : CategoryBase
{
}
I'm using Entity Framework 4 CTP5 code first approach and I have a Table per Hierarchy (TPH) mapping. Some of my classes in the hierarchy have properties in common.
public class BaseType
{
public int Id { get; set; }
}
public class A : BaseType
{
public string Customer { get; set; }
public string Order { get; set; }
}
public class B : BaseType
{
public string Customer { get; set; }
public string Article { get; set; }
}
public class C : BaseType
{
public string Article { get; set; }
public string Manufacturer { get; set; }
}
The default convention maps this to the following columns:
Id
Article1
Article2
Customer1
Customer2
Manufacturer
Order
Type
I want to have EF4 share the common properties to end up with the following:
Id
Article
Customer
Manufacturer
Order
Type
Apart from the reduced number of columns, this has the advantage of being able to search for records based on Article for example, without having to know which types exactly have an Article property.
I tried mapping each common property to the same column:
modelBuilder.Entity<B>().Property(n => n.Article).HasColumnName("Article");
modelBuilder.Entity<C>().Property(n => n.Article).HasColumnName("Article");
but this threw the following exception:
Schema specified is not valid. Errors: (36,6) : error 0019: Each property name in a type must be unique. Property name 'Article' was already defined.
Does anyone know how to get around this validation rule?
There is no workaround to bypass this validation. In TPH a column is either belongs to the base class which is inherited by all childs or is specialized to the child class. You cannot instruct EF to map it to two of your childs but not for the other. Attempting to do so (for example by putting [Column(Name = "Customer")] on both A.Customer and B.Customer) will be causing a MetadataException with this message:
Schema specified is not valid. Errors:
(10,6) : error 0019: Each property name in a type must be unique. Property name 'Customer' was already defined.
TPH Solution:
One solution to this would be to promote Customer and Article properties to the base class:
public class BaseType {
public int Id { get; set; }
public string Customer { get; set; }
public string Article { get; set; }
}
public class A : BaseType {
public string Order { get; set; }
}
public class B : BaseType { }
public class C : BaseType {
public string Manufacturer { get; set; }
}
Which results to the desired schema:
TPT Solution (Recommended):
That said, I recommend to consider using Table per Type (TPT) since it's a better fit for your scenario:
public class BaseType
{
public int Id { get; set; }
}
public class A : BaseType
{
[Column(Name = "Customer")]
public string Customer { get; set; }
public string Order { get; set; }
}
public class B : BaseType
{
[Column(Name = "Customer")]
public string Customer { get; set; }
[Column(Name = "Article")]
public string Article { get; set; }
}
public class C : BaseType
{
[Column(Name="Article")]
public string Article { get; set; }
public string Manufacturer { get; set; }
}
public class MyContext : DbContext
{
public DbSet<BaseType> BaseTypes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BaseType>().ToTable("BaseType");
modelBuilder.Entity<A>().ToTable("A");
modelBuilder.Entity<C>().ToTable("C");
modelBuilder.Entity<B>().ToTable("B");
}
}
For anyone who was having trouble with this issue, it has now been fixed in EF6:
Entity framework - Codeplex