I'm struggling with TPT inheritance in MVC - EF.
I have an AAnimal abstract class and two classes that inherit it, Zebra and Lion. There is a Cage class which holds an AAnimal.
My problem is that because AAnimal is abstract, the EF cannot create an instance of it when I load all Cages. So what I want is a way to override this behavior and make it understand whether it needs to load a Zebra or a Lion.
Zebra and Lion have a Primary key which is also foreign key to Animal table. This is done by the EF (TPT inheritance model).
public abstract class AAnimal
{
[Key]
public int AnimalId { set; get; }
public string Name { set; get; }
}
public class Lion : AAnimal {}
public class Zebra : AAnimal {}
public class Εmployee
{
[Key]
public int EmployeeId { set; get; }
public string Name { set; get; }
}
public class Cage
{
[Key]
public int CageId { set; get; }
[ForeignKey("CagedAnimal")]
public int CagedAnimalId { set; get; }
public AAnimal CagedAnimal { set; get; }
[ForeignKey("CageEmployee")]
public int CageEmployeeId { set; get; }
public Employee CageEmployee { set; get; }
}
// Model mapping
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AAnimal>().ToTable("Animal");
modelBuilder.Entity<Lion>().ToTable("Lion");
modelBuilder.Entity<Zebra>().ToTable("Zebra");
modelBuilder.Entity<Εmployee>().ToTable("Εmployee");
}
// Load all cages
public ActionResult Index()
{
var allCages = db.Cages.ToList();
}
At this point all cages are loaded, all fields have values except the CagedAnimal which is null. Even the CagedAnimalId has value.
How can I tell the EF to follow the same procedure while saving data, in order to load entities?
Note that this is just an example. Also, TPT inheritance model has been selected over other inheritance models.
Related
I am using Entity Framework code first to try and create a database whereby I have several tables that share unique primary keys.
I have the following structure to my domain classes, and would hope that FirstClass, SecondClass, ThirdClass would all be created as tables that link to a BaseTable where the shared keys are stored. I have included the SharedClass class in this example since I also have this strutcure in my project.
[Table("BaseTable")]
public abstract class BaseClass {
public int Id { get; set; }
}
public abstract class SharedClass : BaseClass {
public string SharePropertyOne { get; set; }
public string SharePropertyOne { get; set; }
}
[Table("FirstClass")]
public abstract class FirstClass : SharedClass {
public string SharePropertyOne { get; set; }
public string SharePropertyOne { get; set; }
}
[Table("SecondClass")]
public abstract class SecondClass : SharedClass {
public string SharePropertyOne { get; set; }
public string SharePropertyOne { get; set; }
}
[Table("ThirdClass")]
public abstract class ThirdClass : SharedClass {
public string SharePropertyOne { get; set; }
public string SharePropertyOne { get; set; }
}
What is currently happening is that FirstClass, SecondClass and ThirdClass are all being created but with their own primary keys and no BaseTable is being created.
I am not sure if I am missing something after looking through several questions on here that are asking something similar.
The EF inheritance is not activated by default and even by applying Table data annotation on a base class.
It can be activated by either adding a DbSet to the derived DbContext:
public DbSet<BaseClass> BaseClass { get; set; }
which also allows you to use polymorphic queries, or with fluent configuration - the bare minimum needed is:
modelBuilder.Entity<BaseClass>();
I have tried several things before coming here, such as different model approach, annotation, declarations in DbContext, different fluent API usages but I can't seem to see what the issue is.
I have a YogaClass record but when I iterate over the subscriptions from a person, I have a subscription but no YogaClass (NULL) and yes I.Include(Person.Subscriptions) when querying the DB, I'm getting the subs but not the relational YogaClass/WorkShop associated with it.
In short, I have the following classes :
Subscription (base class)
public class Subscribtion
{
[Key]
public int SubscribtionID { get; set; }
public Person Person { get; set; }
public bool IsPayed { get; set; }
}
WorkshopSubscription (inherrits Subscription)
public class WorkshopSubscribtion : Subscribtion
{
[Key]
public int WorkshopSubscribtionID { get; set; }
public Workshop Workshop { get; set; }
}
YogaClassSubscription (inherrits Subscription)
public class YogaClassSubscribtion : Subscribtion
{
[Key]
public int YogaClassSubscribtionID { get; set; }
public YogaClass YogaClass { get; set; }
}
YogaClass (base class)
public class YogaClass
{
[Key]
public int YogaClassID { get; set; }
public List<Subscriptions> Subscriptions { get; set; }
}
WorkShop (base class)
public class WorkShop
{
[Key]
public int WorkShopID { get; set; }
public List<Subscriptions> Subscriptions { get; set; }
}
Now after I insert some records in the Seeder I have the following issue when I look into my DataBase:
Table Subscription : SubscriptionID:1 , WorkShop_WorkShopID:NULL , YogaClass_YogaClassID:NULL . (Why are they both NULL ?)
Table YogaClassSubscription : SubscriptionID:1 , YogaClassID:1
Same for workshop.
I don't get why the FK from Yoga & Workshop Subscription is NULL in Subscription table.
I have a DbSet declared in my Context and in modelBuilder fluent API method I have mapped both YogaClassSubscribtion & WorkShopSubscribtion to their own table.
public DbSet<Subscribtion> Subscribtions { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<WorkshopSubscribtion>().ToTable("WorkshopSubscribtions");
modelBuilder.Entity<YogaClassSubscribtion>().ToTable("YogaClassSubscribtions");
}
I think your problem comes from using the base class Subscription as the type of your generic lists Subscriptions. You should use the specific derived classes YogaClassSubscription and WorkShopSubscription:
public class YogaClass
{
[Key]
public int YogaClassID { get; set; }
public virtual List<YogaClassSubscription> Subscriptions { get; set; }
}
public class WorkShop
{
[Key]
public int WorkShopID { get; set; }
public virtual List<WorkShopSubscription> Subscriptions { get; set; }
}
This way EF knows about the relationship between WorkShops and WorkShopSubscriptions, and between YogaClasses and YogaClassSubscriptions.
Another thing that seems wrong: redefining the ID property and the Key annotations in your derived classes. You only need to define the ID in the base class. Remove those properties and their annotations. EF will create a foreign key with a one-to-one relationship between your base class table and the derived classes tables.
public class WorkshopSubscription : Subscription
{
public virtual Workshop Workshop { get; set; }
}
public class YogaClassSubscription : Subscription
{
public virtual YogaClass YogaClass { get; set; }
}
An advice: define your navigation properties as virtual, in order to allow EF to use proxies to track status changes in your entities, and also to allow the use of Lazy Loading.
I've got a base class MyBase which is a part of my data model. I also have classes MyChild1, MyChild2 derived from it in related assemblies.
I want the children to be stored in database and loaded just like MyBase. Also I don't want my entity configuration to know anything about children classes.
Is there any way to force EF to ignore that inheritance and user only base class?
Thanks in advance
Here is your case:
public abstract class BaseModel
{
public int Id { get; set; }
public string Name { get; set; }
}
public partial class Child1 : BaseModel
{
public string Name1 { get; set; }
}
public partial class Child2 : BaseModel
{
public string Name2 { get; set; }
}
I guess your data should be similar like this (I am not sure your detail requirements, here is just an example):
public partial class Example<T> where T: BaseModel
{
public int Id { get; set; }
public T Data { get; set; } // so here could be any Child of BaseModel
}
public partial class Example: Example<BaseModel>
{
}
Use ModelBuilder.Ignore<>() Method to let EFCore ignore your children and base.
User PropertyBuilder.HasConversion Method to convert your data to/from database.
Here is sample code:
public class MyDbContext : DbContext
{
public virtual DbSet<Example> Examples { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
//Let EFCore ignore all models that you don't want it to be a table
builder.Ignore<BaseModel>();
builder.Ignore<Child1>();
builder.Ignore<Child2>();
builder.Entity<Example>(entity =>
{
entity.Property(p => p.Data).HasConversion(
x => JsonConvert.SerializeObject(x) //convert TO a json string
, x => JsonConvert.DeserializeObject<BaseModel>(x)//convert FROM a json string
);
});
}
}
I was curious if it is possible to map an intermediate table through a containing object.
public class Subscriber : IEntity
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
private ChannelList _subscribedList { get; set; }
public int NumSubscribedChannels { get { return _subscribedList.Count(); } }
}
public class HelpChannel : IEntity
{
[Key]
public int Id { get; set; }
public string name { get; set; }
public string category { get; set; }
public int group { get; set; }
}
I need to have a subscriber table, channel table and an intermediate table to link a subscriber to his/her channels.
Is it possible to map the list that is within the ChannelList object to the Subscriber Model?
I figured that's probably not possible and that I would need to just have a private List for EF to map. But I wasn't sure if EF will do that for private variables. Will it?
I'm hoping that is does because if it has to be public to maintain the encapsulation.
You can map private properties in EF code-first. Here is a nice description how to do it. In your case it is about the mapping of Subscriber._subscribedList. What you can't do is this (in the context's override of OnModelCreating):
modelBuilder.Entity<Subscriber>().HasMany(x => x._subscribedList);
It won't compile, because _subscribedList is private.
What you can do is create a nested mapping class in Subscriber:
public class Subscriber : IEntity
{
...
private ICollection<HelpChannel> _subscribedList { get; set; } // ICollection!
public class SubscriberMapper : EntityTypeConfiguration<Subscriber>
{
public SubscriberMapper()
{
HasMany(s => s._subscribedList);
}
}
}
and in OnModelCreating:
modelBuilder.Configurations.Add(new Subscriber.SubscriberMapping());
You may want to make _subscribedList protected virtual, to allow lazy loading. But it is even possible to do eager loading with Include:
context.Subscribers.Include("_subscribedList");
Getting an error when trying to set a ForeignKeyAttribute in a base class
class User { }
abstract class FruitBase
{
[ForeignKey("CreateById")]
public User CreateBy{ get; set; }
public int CreateById{ get; set; }
}
class Banana : FruitBase { }
class DataContext : DbContext
{
public DbSet<Banana> Bananas { get; set; }
}
If I move the FruitBase code into the banana, all is well, but I don't want to, as there will be many many fruit and I want to remain relatively DRY if I can
Is this a know issue that will be fixed by March?
Does anyone know a work around?
The problem caused by the fact that in your DbContext you put DbSet<Banana> instead of DbSet<FruitBase>. The following object model works as expected:
public class User
{
public int UserId { get; set; }
}
public abstract class FruitBase
{
public int Id { get; set; }
public int CreateById { get; set; }
[ForeignKey("CreateById")]
public User CreateBy { get; set; }
}
public class Banana : FruitBase { }
public class DataContext : DbContext
{
public DbSet<FruitBase> Fruits { get; set; }
}
You have to be aware that by doing this, you are essentially creating a Polymorphic Association and as of CTP5, not all of the inheritance mapping strategies allow polymorphic association. Here it works fine because you've used Table per Hierarchy (TPH).
Update: Use Table per Type (TPT) Strategy:
Polymorphic Associations work with TPT as well:
public class StackoverflowTestContext : DbContext
{
public DbSet<FruitBase> Fruits { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Banana>().ToTable("Bananas");
}
}
as discussed above, this is TPC (Table per Concrete Type), the answer/work around here is to take the ForeignKeyAttribute out of the base
class User{}
abstract class AuditObjectBase{ // was FruitBase
// [ForeignKey("CreateById")]
public abstract User CreateBy{ get; set; } // made abstract
public int CreateById{ get; set; } // both get and set required public
}
class ItemOne : AuditObjectBase{ // Was Banana
// added
[ForeignKey("CreateById")]
public override User CreateBy{ get; set; }
}
class ItemTwo : AuditObjectBase{ // Added
[ForeignKey("CreateById")]
public override User CreateBy{ get; set; }
}
class DataContext : DbContext{
DbSet<ItemOne> ItemOnes{ get; set; }
DbSet<ItemTwo> ItemTwos{ get; set; }
}
Not completely DRY but at least when you create an object which uses the AuditObjectBase it will force you to implement the Foreign Key property, some nice comments to remind you of the attribute and away you go
Basically it seams, attributes should be added to the class declared in the DbContext as an Entity, in this case has a DbSet property.