So I've implement Table Per concrete Class to deal with an inheritance hierarchy, but I'm having trouble with Navigation properties.
My Model is structure as follows:
I have an abstract BaseEntity class, with Multiple derived classes, so:
public abstract class BaseEntity
{
public virtual ICollection<Comment> Comments { get; set; }
public EntitytType EntitytType { get; set; }
}
public class FirstDerivedEntity : BaseEntity
{
//EntitytType == First;
}
public class SecondDerivedEntity : BaseEntity
{
//EntitytType == Second;
}
public class Comment
{
public long BaseEntityId { get; set; }
public EntitytType BaseEntityType { get; set; }
}
public enum EntitytType
{
First,
Second
}
The Comments navigation property here doesn't work because each derived(Concrete) class has it's own set of Ids. In my mind the EntityType column in each Table would serve as some sort of Discriminator, but I don't know how to tell EF to use it as such.
Any help would be greatly appreciated!
The solution in order for TPC to work would be the Ids in the derived classes to be unique not only in their table but in both tables.
You can use a database solution like an auto increment primary key with different initial seeds or GUID keys like the ones in SQL server.
Another approach would be to generate unique GUID keys in you application code.
You can see same sample code of how to model the entities below :
namespace tpc_so
{
public class tpc_soContext : DbContext
{
public tpc_soContext()
{
}
public DbSet<BaseEntity> BaseEntities { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<BaseEntity>().HasKey(b => b.BaseEntityId);
modelBuilder.Entity<BaseEntity>()
.Property(b => b.BaseEntityId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<FirstDerivedEntity>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("FirstDerivedEntities");
});
modelBuilder.Entity<SecondDerivedEntity>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("SecondDerivedEntities");
});
modelBuilder.Entity<Comment>().ToTable("Comments");
base.OnModelCreating(modelBuilder);
}
}
public abstract class BaseEntity
{
public Guid BaseEntityId { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class FirstDerivedEntity : BaseEntity{}
public class SecondDerivedEntity : BaseEntity{ }
public class Comment
{
public long CommentId { get; set; }
public Guid BaseEntityId { get; set; }
public string Text { get; set; }
}
}
And to create some entities use the code below :
using (var ctx = new tpc_soContext())
{
ctx.BaseEntities.Add(new FirstDerivedEntity()
{
BaseEntityId = Guid.NewGuid(),
Comments = new List<Comment>() { new Comment() { Text = "First Derived Comment" } }
});
ctx.BaseEntities.Add(new SecondDerivedEntity()
{
BaseEntityId = Guid.NewGuid(),
Comments = new List<Comment>() { new Comment() { Text = "Second-Derived Comment" } }
});
ctx.SaveChanges();
}
Some good sources for TPC would be here :
[Inheritance with EF Code First: Part 3 – Table per Concrete Type (TPC)]
[Entity Framework - Table Per Concrete]
Related
Thank you very much in advance,
I have an abstract class called Person which has a foreign key for theCity table.
public abstract class Person
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("id")]
public Int32 Id { get; set; }
[Column("city")]
public int CityId { get; set; }
[Required]
[ForeignKey("CityId")]
public City City
{
get; set;
}
}
[Table("cities", Schema = "public")]
[Serializable]
public class City
{
public City()
{
}
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Key]
[Column("id")]
public Int32 Id { get; set; }
[Column("name")]
public String Name { get; set; }
}
And later I have my Customer class that inherits fromPerson:
public class Customer:Person
{
[Column("cpfcnpj")]
public String CpfCnpj { get; set; }
}
And I'm mapping the TPC (Type per Concrete) inheritance as follows in my context class:
public class MyContext:DbContext
{
public DbSet<Person> People { get; set; }
public MyContext():base("MyConnection")
{
Configuration.LazyLoadingEnabled = false;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Conventions.Remove();
modelBuilder.Entity<Customer>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("customers", "public");
});
base.OnModelCreating(modelBuilder);
}
}
When I try to pick up my customers I get the following error:
Person: : The referenced EntitySet 'Person' for End 'Person' could not
be found in the containing EntityContainer.
The code used to select customers is the following:
using (var context = new MyContext())
{
//get error in this line
var customers = context.People.OfType<Customer>().ToList();
//...
}
Note: To test, I passed the foreign key properties for the Customer concrete class and executed perfectly.
Can someone help me? I really need to understand this situation.
Thanks.
I remember there were problems with generic entities in previous EF. How about EF Core? I can't find docs related to this matter.
For example:
public abstract class Parent<TEntity> {
public int EntityId { get; set; }
public TEntity Entity { get; set; }
}
public class Child : Parent<Foo> {
}
public class OtherChild : Parent<Bar> {
}
// config for child entities includes this:
config.HasKey(c => c.EntityId);
Though this throws stating that child entities do not define a primary key, when they clearly do!
I can fix this by making Parent non-generic.
Are there official docs for this? Am I doing something wrong, or is this the expected behavior?
I can use this model in ef-core 1.1.0:
public abstract class Parent<TEntity>
{
public int EntityId { get; set; }
public TEntity Entity { get; set; }
}
public class Child : Parent<Foo>
{
}
public class OtherChild : Parent<Bar>
{
}
public class Foo
{
public int Id { get; set; }
}
public class Bar
{
public int Id { get; set; }
}
With this mapping in the context:
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Child>().HasKey(a => a.EntityId);
mb.Entity<Child>().HasOne(c => c.Entity).WithMany().HasForeignKey("ParentId");
mb.Entity<OtherChild>().HasKey(a => a.EntityId);
mb.Entity<OtherChild>().HasOne(c => c.Entity).WithMany().HasForeignKey("ParentId");
}
Which leads to this fantastic model:
public class BasePage
{
public int Id { get; set; }
...
}
public class Region : BasePage
{
public virtual MapCoordinates Map { get; set; }
...
}
public class Place: BasePage
{
public virtual MapCoordinates Map { get; set; }
...
}
public class MapCoordinates
{
[Key, ForeignKey("BasePage")]
public int Id { get; set; }
public virtual BasePage BasePage { get; set; }
...
}
Getting this exception on SaveChanges:
Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.
use this code :
public class BasePage
{
public int Id { get; set; }
public virtual MapCoordinate Map { get; set; }
...
}
public class Region : BasePage
{
...
}
public class Place: BasePage
{
...
}
public class MapCoordinate
{
[Key]
public int BasePageId { get; set; }
...
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<BasePage>()
.HasRequired(e => e.MapCoordinate)
.WithRequiredPrincipal();
modelBuilder.Entity<BasePage>().ToTable(BasePages);
modelBuilder.Entity<MapCoordinate>().ToTable(MapCoordinates);
}
check out these links :
Associations in EF Code First: Part 4 – Table Splitting
Associations in EF Code First: Part 5 – One-to-One Foreign Key Associations
Code First and Fluent API one to one relationship
All my attempts to map the ID of SitePage to the database column ID (SitePages table, ID column of type bigint) has failed. It keeps looking for column SitePage_ID to map it.. Can you see where I am doing wrong? All related code is below;
public class Site : EntityBase<Int64>
{
public virtual string Url { get; set; }
public virtual IList<SitePage> Pages { get; set; }
}
public class SitePage : EntityBase<Int64>
{
public virtual Site Site { get; set; }
public virtual string Url { get; set; }
public virtual string Html { get; set; }
public virtual string Text { get; set; }
public virtual string Language { get; set; }
}
public abstract class EntityBase<T> : IComparable
{
public virtual T ID { get; set; }
protected EntityBase() : this(default(T))
{
}
protected EntityBase(T id)
{
this.ID = id;
if (this.ID == null)
this.ID = default(T);
}
}
public class SpellCrawlerContext : DbContext
{
public SpellCrawlerContext(){}
public DbSet<Site> Sites { get; set; }
public DbSet<SitePage> SitePages { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Site>()
.HasMany(s => s.Pages)
.WithRequired(p => p.Site)
.Map(s => s.MapKey("SiteID"));
modelBuilder.Entity<SitePage>()
.HasKey(p => p.ID);
modelBuilder.Entity<SitePage>()
.Property(p => p.ID)
.HasColumnName("ID");
}
}
You are not doing anything wrong. The code you shown does everything correctly. You even don't need to explicitly define the name of ID in SitePage because it will be defined like ID anyway.
SitePage_ID is used by default naming convention for foreign keys created for independent associations. So do you have any other one-to-many relation between SitePage and any other entity? If you didn't map foreign key in dependent entity it will be defined as SitePage_ID by default.
I have a POCO class that has two one-way unary relationships with another class, both classes share an ancestor. The names of the foreign keys in the generated schema do not reflect the property names. (Properties MainContact and FinancialContact give PersonId and PersonId1 field names).
How can I influence schema generation to generate database column names that match the property names?
The model looks like this:
The code looks like this:
public class CustomerContext: DbContext
{
public DbSet<Organisation> Organisations { get; set; }
public DbSet<Person> Persons { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
DbDatabase.SetInitializer(new DropCreateDatabaseAlways<CustomerContext>());
}
}
public abstract class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Person : Customer
{
public string Email { get; set; }
}
public class Organisation : Customer
{
public Person FinancialContact { get; set; }
public Person MainContact { get; set; }
}
The schema looks like this:
Answer from druttka
druttka's answer below did the job and it's nice to know that it's a CTP5 bug that's behind this. EF also needs the cascade behaviour to be specified and I've used the fluent API to do this following the example in the link given by druttka. Some more good reading from Morteza Manavi here.
The code now is this:
public class CustomerContext : DbContext
{
public DbSet<Organisation> Organisations { get; set; }
public DbSet<Person> Persons { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
DbDatabase.SetInitializer(new DropCreateDatabaseAlways<CustomerContext>());
builder.Entity<Organisation>()
.HasRequired(p => p.MainContact)
.WithMany()
.HasForeignKey(p => p.MainContactId)
.WillCascadeOnDelete(false);
builder.Entity<Organisation>()
.Property(p => p.MainContactId)
.HasColumnName("MainContact");
builder.Entity<Organisation>()
.HasRequired(p => p.FinancialContact)
.WithMany()
.HasForeignKey(p => p.FinancialContactId)
.WillCascadeOnDelete(false);
builder.Entity<Organisation>()
.Property(p => p.FinancialContactId)
.HasColumnName("FinancialContact");
}
}
public abstract class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Person : Customer
{
public string Email { get; set; }
}
public class Organisation : Customer
{
public Person FinancialContact { get; set; }
public int FinancialContactId { get; set; }
public Person MainContact { get; set; }
public int MainContactId { get; set; }
}
Which now gives the far more suitable database:
EF Code First uses, by default, convention over configuration. However, you can set explicit alternatives by overriding DbContent.OnModelCreating. Many examples here, courtesy of ScottGu.
EDIT
So in CTP5, MapSingleType went away as described here. The following works for simple string properties, but not for your Organisation to Person relationships. I'm curious and plan to keep looking at it, but in the meantime, maybe this will get your started or someone else can complete the answer.
public class Person : Customer
{
[Column(Name="EmailAddress")]
public string Email { get; set; }
}
EDIT 2
Ok, this gets it. Found the answer here. Disclaimer: I've only verified that the database schema is created as expected. I have not tested that seeding data or further CRUD operations work as expected.
public class Organisation : Customer
{
[Column(Name = "FinancialContact")]
public int? FinancialContactId { get; set; }
[ForeignKey("FinancialContactId")]
public Person FinancialContact { get; set; }
[Column(Name = "MainContact")]
public int? MainContactId { get; set; }
[ForeignKey("MainContactId")]
public Person MainContact { get; set; }
}