Entity Framework: TPC MapInheritedProperties model super class properties - c#

Summary: in Entity Framework I use TPC to create two classes derived from the same base class. In fluent API I map inherited properties, but how to model the properties of the base class?
More extensive description
In Entity Framework I have a class Child, and two kinds of Children: a Boy and a Girl. Both Boy and Girl derive from Child:
public class Child
{
public int Id {get; set;}
public string Name {get; set;}
}
public class Boy : Child
{
public string SomeBoyishProperty {get; set;}
}
public class Girl : Child
{
public string SomeGirlyProperty {get; set;}
}
I want a table with boys and a table with girls, each table also having the Child properties.
public class MyDbContext : DbContext
{
public DbSet<Boy> Boys {get; set;}
public DbSet<Girl> Girls {get; set;
}
From several sources, for example this one I learned that this is called TPC: table per concrete class and that I should MapInheritedProperties in OnModelCreating
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// model the properties of the base class, for instance set max length
modelBuilder.Entity<Child>()
.Property(p => p.Name).IsRequired().HasMaxLength(12);
// Model Daughter:
modelBuilder.Entity<Daughter>()
.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Daughters");
})
.Property(p => p.SomeGirlyProperty).IsOptional().HasMaxLength(13);
// model Boy
modelBuilder.Entity<Son>()
.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Sons");
})
.Property(p => p.SomeBoyishProperty).IsOptional().HasMaxLength(14);
}
During SaveChanges I get an InvlidOperationException indicating that the primary key is not unique. Removing the part that builds Child solves this problem.
How to build the Child properties without having to do this in the Girl and again in the Boy properties?

SHORT ANSWER:
If you want your code to work, remove any reference to Child entity in your model configuration. As soon as EF knows about Child as Entity it will enforce the following rule: There cannot be 2 entities of type Child or 2 entities that inherit from Child with the same PK in memory. You can see the error tells you that the entities where successfully persisted; but when EF pulls the new IDs it finds out both have the same ID.
LONG ANSWER
Remove
modelBuilder.Entity<Child>()
.Property(p => p.Name).IsRequired().HasMaxLength(12);
Instead this is how your OnModelCreating method should look like.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Model Daughter:
var girlEntity = modelBuilder.Entity<Girl>();
girlEntity.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Daughters");
});
girlEntity.Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
girlEntity.Property(p => p.Name).IsRequired().HasMaxLength(12);
girlEntity.Property(p => p.SomeGirlyProperty).IsOptional().HasMaxLength(13);
// model Boy
var boyEntity = modelBuilder.Entity<Boy>();
boyEntity.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Sons");
});
boyEntity.Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
boyEntity.Property(p => p.Name).IsRequired().HasMaxLength(12);
boyEntity.Property(p => p.SomeBoyishProperty).IsOptional().HasMaxLength(14);
}
If you don't want the configuration repetition in the configuration I would use the DataAnnotations attributes on the base class to enforce the name to be required.
You will also need to enforce that the Id property to be auto-generated in database. This doesn't happen by convention when using the Map method in the fluent API. You can see I added the fluent calls to make that happen in both the Girl and Boy mapping.
Hope this helps.

I reworked Arturo's solution proposal. This solution was too long to
describe as a comment. So Arturo: thanks for giving me the ideas.
Chapeau!
Arturo suggested to use Data Annotations. The reason that I don't want to use that method is, that the modelling of the class doesn't necessary correspond with a certain database representation. I bit hypothetical, but if I want a smaller maximum length for a boy's Name than for a Girl's name, then a data annotation wouldn't help.
Besides, there are some things that has to be done using fluent API. For instance, you can't say that a System.DateTime has a DateTime2 format in the database using DataAnnotations.
If you didn't guess it already: my problem description was highly simplified. All three classes have a lot of properties that need a lot of fluent API configurations
Arturo's remarks helped me to the following solution:
internal class ChildConfig<T> : EntityTypeConfiguration<T> where T : Child
{
public ChildConfig(...)
{
// configure all Child properties
this.Property(p => p.Name)....
}
}
internal class BoyConfig : ChildConfig<Boy>
{
public BoyConfig(...) : base (...)
{
// the base class will configure the Child properties
// configure the Boy properties here
this.Property(p => p.SomeBoyishProperty)...
}
}
And in MyDbContext:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new BoyConfig(...));
modelBuilder.Configuration.Add(new GirlConfig(...));
}

Related

How to implement non-primitive FK correctly in EF Core via fluent

I've trying to create a new foreign key in entity framework core 6 but I am getting the following error?
The types of the properties specified for the foreign key {'RoleId' : RoleId} on entity type 'SomeEntity' do not match the
types of the properties in the principal key {'Id' : Guid} on entity
type 'DbsRole'. Provide properties that use the same types in the same
order.
How can I fix this via Fluent API without adding navigation properties to my entities as I am following DDD principles (bounded contexts etc)?
I thought the conversion handler would be sufficient but obviously not.
RoleId.cs
public record RoleId(Guid Value);
SomeEntity.cs
public class SomeEntity : Entity<Guid>, IAggregateRoot
{
public Guid Id {get; private set;} = null!;
public string Name { get; private set; } = null!;
public RoleId RoleId { get; private set; } = null!;
}
MyContext.cs
public partial class MyContext : IdentityDbContext<MyUser, MyRole, Guid>
{
public virtual DbSet<SomeEntity> SomeEntity { get; set; } = default!;
//... snip...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
_ = modelBuilder.Ignore<RoleId>();
_ = modelBuilder.Entity<SomeEntity>(entity =>
{
_ = entity.HasKey(e => e.Id);
_ = entity.Property(e => e.Name)
.IsRequired();
_ = entity.HasIndex(e => e.Name);
_ = entity.Property(e => e.RoleId)
.IsRequired()
.HasConversion(x => x.Value, x => new RoleId(x));
_ = entity.HasOne<MyRole>()
.WithMany()
.IsRequired()
.HasForeignKey(p => p.RoleId);
});
}
}
MyUser.cs
public class MyUser : IdentityUser<Guid>
{
//... snip...
}
MyRole.cs
public class MyRole : IdentityRole<Guid>
{
// ...snip...
}
Regards
Kyle
How can I fix this via Fluent API without adding navigation properties to my entities as I am following DDD principles (bounded contexts etc)?
You can't (at least currently - up to EF Core 6.0 inclusive). Navigation properties have nothing to do with the problem, which is the type of the PK and FK properties. The error message is quite clear of what is expected
Provide properties that use the same types...
You may consider it EF Core limitation/shortcoming, but it is what it is, and you must follow EF Core data model rules if you want to use it for persisting your domain model.
Which in general means you shouldn't be using the domain model as data model directly. Bounded contexts, value objects, encapsulation - these are not natural for data models. With small exceptions, EF Core context represents a database, DbSet is a table, entity is a record in that table, and navigation properties are relationships. So the best would be if you create separate models and map between the two where needed. Yes, it requires additional efforts, but that's the only way you can follow the differences between the two models requirements.
But back on the concrete issue. EF Core requires both properties to have one and the same type. This means you either have to use Guid type for SomeEntity.RoleId, or use RoleId type for MyRole.Id. However the second is not possible with identity model, since both user, role and related things are constrained to have one and the same type of key (the last generic type argument, in your case Guid). So I'm afraid that if you want to use the domain model as data model directly, the only option currently in this particular case is to make SomeEntity.RoleId type Guid.

Explicitly map interface member in Entity Framework 7

I'm attempting to map a class which looks like below but I'm getting an exception regarding the Facility.Projects property being an interface.
The property Facility.Projects is of an interface type IProject. If it is a navigation property manually configure the relationship for this property by casting it to a mapped entity type, otherwise ignore the property from the model.
public class Facility : BaseData
{
[ForeignKey("ClientId")]
public Client Owner { get; set; }
public List<IProject> Projects { get; protected set; }
public Facility()
{
Initialize();
}
private void Initialize()
{
Projects = new List<IProject>();
}
}
I've mapped other properties via the OnModelCreating method for the DbContext class that Facility is mapped to but I'm not really seeing anything that seems like the right spot to cast this other than HasColumnType which also appears wrong.
public class FacilityRepository : BaseRepository<Facility>, IFacilityRepository
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder
.Entity<Client>()
.Property(f => f.Id)
.ForSqliteHasColumnName("ClientId");
modelBuilder
.Entity<Project>()
.Property(x => x.Id)
.ForSqliteHasColumnName("ProjectId");
// area where I've tried and failed to find a good spot to do some manual casting
modelBuilder
.Entity<Facility>()
.Property(f => f.Projects)
.HasColumnType<Project>(new PropertyBuilder());
}
}
Can anyone point me in the right direction? I've not seen anything in the documentation that might help? Or should I, by rule, not be doing this at all?
To do a manual mapping of a one-to-many relationship (this example is just exposing the children on the parent, not vice versa), you can set this up as follows, and then it's pretty straightforward where the cast should be:
modelBuilder.Entity<ProjectState>()
.HasMany(h => (ICollection<ProjectRoleState>)h.ProjectRoleStates)
.WithOne()
.HasForeignKey(p=>p.ProjectGuid);

Entity Framework Self Referencing Using Non-Primary Key Column

I have an employee table that self references to determine organization structure. I'm having some trouble trying to set this up using Code-First (POCO) fluently.
An employee record has both a "Position" field and a "ReportsTo" field and neither of the columns are the primary key (employee.id).
An employee with a "ReportsTo" value of "08294" , is an employee of a direct report of an employee with "Position" value of "08294".
Can anyone offer up some info on how to set this up using EF code first, fluently...is it possible?
I tried the code below and am getting error:
Employee_Employees_Source_Employee_Employees_Target: : The types of
all properties in the Dependent Role of a referential constraint must
be the same as the corresponding property types in the Principal Role.
The type of property 'ReportsTo' on entity 'Employee' does not match
the type of property 'Id' on entity 'Employee' in the referential
constraint 'Employee_Employees'.
Employee.cs
public class Employee
{
public int Id { get; set; } //pk
public string Position { get; set; } // i.e. 06895
public string ReportsTo{ get; set; } // i.e. 08294
public virtual Employee Supervisor { get; set; }
public virtual ICollection<Employee> Employees { get; set; }
}
DbContext
modelBuilder.Entity<Employee>()
.HasMany(e => e.Employees)
.WithOptional(e => e.Supervisor)
.HasForeignKey(e => e.ReportsTo);
I think more than anything, I would like to keep the POCO free of EF "stuff" and be able to do something like:
employee.IsSupervisor(); // based on child employee count.
The issue is in the relationship configuration. If you want to configure your one to many relation without using a FK, you could do this:
modelBuilder.Entity<Employee>()
.HasMany(e => e.Employees)
.WithOptional(e => e.Supervisor);
Now if you want to use a FK property, then add this property to your model class:
public class Employee
{
//...
public int SupervisorId { get; set; }
}
And map your relationship this way:
modelBuilder.Entity<Employee>()
.HasMany(e => e.Employees)
.WithOptional(e => e.Supervisor)
.HasForeignKey(e => e.SupervisorId);
To resolve your issue related with ReportTo and Position properties,I think you should handle that logic in your code. If you want to know if an Employee is a supervisor based on the count of Employees property, you could use a NotMapped property:
public class Employee
{
[NotMapped]
public bool IsSupervisor
{
get
{
return Employess.Count>0
}
}
}
You can do the same using Fluent Api:
modelBuilder.Entity<Employee>().Ignore(e => e.IsSupervisor);
PS: Remember initialize Employees in your class'constructor.
The error you get is because it is trying to map a PK of int type to a FK of string type. User int for all of your key fields.
Then, you need to declare your OnModelBuilding like this:
modelBuilder.Entity<Employee>()
.HasOptional(e => e.Supervisor)
.WithMany()
.HasForeignKey(s => s.ReportsTo);
To get something like IsSupervisor() you can take advantage of partial classes. Create another class file which is a public partial class Employee (and modify your original one to be partial), then in your new file you will add a property that does whatever you want, and decorate it with [NotMapped] attribute. Yours will probably look something like public bool IsSupervisor {get { return (Employees == null) ? false : true; } set {} } The new partial class is where you can do whatever you want for the POCO without changing the EF class (make sure you use [NotMapped] though).

Should I map both sides of bidirectional relations in EF code first?

Assume I have the following entity classes:
public class Customer {
public int Id { get; set; }
public virtual ICollection<Order> Orders { get; set; }
}
public class Order {
public int Id { get; set; }
public virtual Customer Customer { get; set; }
}
How should those be mapped in Entity Framework 6 fluent code-first mapping? I want to be explicit about the mapping and not rely on automatic mapping conventions.
Option 1
Just map the local properties of both classes. That's how I would do it in Fluent NHibernate.
public class CustomerMap : EntityTypeConfiguration<Customer> {
public CustomerMap() {
HasMany(x => x.Orders);
}
}
public class OrderMap : EntityTypeConfiguration<Order> {
public OrderMap() {
HasRequired(x => x.Customer);
}
}
Option 2
Map both sides of the relationship in both classes.
public class CustomerMap : EntityTypeConfiguration<Customer> {
public CustomerMap() {
HasMany(x => x.Orders).WithRequired(x => x.Customer);
}
}
public class OrderMap : EntityTypeConfiguration<Order> {
public OrderMap() {
HasRequired(x => x.Customer).WithMany(x => x.Orders);
}
}
Option 3
Map both sides of the relation, but only in one of the classes. The code would be similar to option 2, just one of the two constructors would be empty.
Is there any difference between those options? If yes, please also explain why I should or shouldn't use a specific option.
I would go for option 3.
In option 1 you can forget to map the inverse end of an association. In this simple example it's clear that Order.Customer and Customer.Orders are two ends of the same association. When things get more complex, this isn't always obvious. Also, it is redundant code.
In option 2 you could have conflicting mappings. For instance when you have...
HasOptional(x => x.Customer).WithMany(x => x.Orders);
...in OrderMap, you will get a runtime exception telling you that both mappings don't match. And again, it is redundant code.
So option 3 is DRY and safe. The only issue is that it's a bit arbitrary where to configure the mappings. I tend to adhere to mapping children in their parent's mapping.
One more comment. You may want to add a primitive property CustomerId in Order. The mapping would look like:
public class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
HasMany(x => x.Orders).WithRequired(x => x.Customer)
.HasForeignKey(o => o.CustomerId);
}
}
Now you have full control over both ends of the association and the foreign key name to be used. Besides that, there are some advantages of these foreign key associations as opposed to independent associations (without a primitive foreign key property). For instance, the ability to establish an association without having to fetch the parent object from the database. You can just by set an Id value.

Conditional Mapping on Relationships

Using Entity Framework (Code First), I'm trying to map a conditional/filtered relationship between the following 2 entities:
Building
BuildingId
BuildingName
Area
AreaId
ParentId
AreaName
IsSubArea
A Building can have many Areas
An Area can have many (Sub)Areas
I would like to create the relationship between Building and Area where the areas marked with 'IsSubArea' are filtered out of the relationship. In this context, ParentId would relate to a Building, otherwise, ParentId would be another Area. This would allow me to create a building with many areas, and each area could have many sub-areas, creating a tree style structure.
The closest to a solution I have found relates to 'soft delete' functionality (source):
modelBuilder.Entity<Foo>().Map(m => m.Requires("IsDeleted").HasValue(false));
Converted to fit my example:
modelBuilder.Entity<Area>().Map(m => m.Requires("IsSubArea").HasValue(false));
But as far as I can tell, this has no bearing on the relationship to the Building.
Another solution would be to create a property on the Building which specifies the query definition to use to return related areas (source):
public class Building
{
public int BuildingId {get; set;}
public string BuildingName {get; set;}
public IQueryable<Area> BuildingAreas
{
get
{
return from area in areas
where area.IsSubArea == false
and area.ParentId == BuildingId
select area;
//Assume I have a reference to relevant DbSets
}
}
}
This solution would work but doesn't feel as elegant as a conditional mapping.
Another solution would be to inherit from Area and create the 2 sub-classes:
BuildingArea
AreaId
BuildingId
AreaName
SubArea
AreaId
ParentAreaId
AreaName
Each inherits from Area and sets the 'IsSubArea' field as appropriate.
This solution feels tidier but I do not know how to implement this in Entity Framework.
Is there a way to specify conditional mapping on relationships?
Is there a better way to implement this structure?
Update 1:Found this & this guide on inheritance which seems to match my requirements. However, neither of these tutorials define relationships between derived types. I'll update the question tonight with what I have tried with regards to the Table per Hierarchy (TPH) method.
Update 2:
I'm going to try an describe the Table per Hierarchy (TPH) method I tried to implement based on the tutorial links above. Forgive me if this gets a little complicated (maybe I'm over thinking it).
Models
The building class remains the same as the OP.
I have created an abstract base class defining the Area properties common to each derived type (BuildingArea and SubArea):
public abstract class Area
{
protected Area(bool isSubArea)
{
IsSubArea = isSubArea;
SubAreas = new List<SubArea>();
}
public int AreaId { get; set; }
public int ParentId { get; set; }
public string AreaName { get; set; }
public bool IsSubArea { get; private set; } //note the private set
public virtual ICollection<SubArea> SubAreas { get; set; }
}
I then have 2 derived types which inherit from Area:
public class BuildingArea : Area
{
public BuildingArea () : base(false)
{}
public virtual Building ParentBuilding { get; set; }
}
public class SubArea : Area
{
public SubArea(): base(true)
{}
// This is of type `Area` because parent could be either `BuildingArea` or `SubArea`
public virtual Area Parent { get; set; }
}
I then have the following 2 EntityTypeConfigurations:
public class BuildingAreaMap : EntityTypeConfiguration<BuildingArea>
{
public BuildingAreaMap ()
{
// Primary Key
HasKey(t => t.AreaId);
// Properties
Property(t => t.AreaName)
.IsRequired()
.HasMaxLength(256);
// Table & Column Mappings
ToTable("Areas");
Property(t => t.AreaId).HasColumnName("AreaId");
Property(t => t.ParentId).HasColumnName("ParentId");
Property(t => t.AreaName).HasColumnName("AreaName");
Property(t => t.IsSubArea).HasColumnName("IsSubArea");
// This is the discriminator column
Map(m => m.Requires("IsSubArea").HasValue(false));
HasRequired(a => a.Site).WithMany(s => s.SiteAreas).HasForeignKey(k => k.ParentId);
}
public class SubAreaMap : EntityTypeConfiguration<SubArea>
{
public SubAreaMap()
{
// Primary Key
HasKey(t => t.AreaId);
// Properties
Property(t => t.AreaName)
.IsRequired()
.HasMaxLength(256);
// Table & Column Mappings
ToTable("AssetHealthAreas");
Property(t => t.AreaId).HasColumnName("AreaId");
Property(t => t.ParentId).HasColumnName("ParentId");
Property(t => t.AreaName).HasColumnName("AreaName");
Property(t => t.IsSubArea).HasColumnName("IsSubArea");
// This is the discriminator column
Map(m => m.Requires("IsSubArea").HasValue(true));
HasRequired(a => a.Parent).WithMany(s => s.SubAreas).HasForeignKey(k => k.ParentId);
}
}
This code builds successfully, but I do get the following runtime error:
Map was called more than once for type 'SiteArea' and at least one of the calls didn't specify the target table name.
But I am specifying the target table name (once in each EntityTypeConfiguration class).
So I removed the EntityTypeConfiguration for SubArea but I get the same error.
One of the tutorials pulls the mapping out of the EntityTypeConfiguration class and puts it in the OnModelCreating handler as follows:
modelBuilder.Entity<Area>()
.Map<BuildingArea>(m => m.Requires("IsSubArea").HasValue(false))
.Map<SubArea>(m => m.Requires("IsSubArea").HasValue(true));
This also gives me the same error.
If I remove relationships from the equation, I get a different error regarding the ParentId property:
The foreign key component 'ParentId' is not a declared property on type 'SiteArea'. Verify that it has not been explicitly excluded from the model and that it is a valid primitive property.
Update 3
An image of the model I'm trying to create...
Update 4
I'm going to try and simplify my model to match the following.
If the solution below works, I will need to have a little more business logic to navigate the tree but it should be manageable.
For the errors when creating the TPH classes:
I think this is because you are not supposed to have the discriminator column as a property in your classes.
Remove the property IsSubArea from your base class.
When you create new instance, EF should automatically detect the type and fill the IsSubArea accordingly.

Categories

Resources