Reputation: 12815
I have a complex object hierarchy in an enterprise application. I'll try and keep it simple, and abstract, yet still representative of what I'm dealing with.
My project deals with several styles of the same type of object. For this, we have implemented the TPT structure for our entity objects:
public abstract class BaseWidget {
public int Id { get; set; }
// etc...
}
// About a dozen concrete implementations already exist and work great!
public class ExistingWidget : BaseWidget {
// Other properties
}
Now I have a new type that I'm doing. We have common properties on the object, but there are a few different sets of details that are required depending on the sub type. For this, I set up TPH, as the properties on that type are the same across all subtypes. The only difference is which details objects are required.
public abstract NewWidgetBase : BaseWidget {
public int EmployeeNumber { get; set; }
public DateTime EffectiveDate { get; set; }
}
public NewWidgetA : NewWidgetBase {
}
public NewWidgetB : NewWidgetBase {
}
I have this mapped in my DbContext like this:
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
modelBuilder.Entity<NewWidgetBase>()
.Map<NewWidgetA>(w => w.Requires("Discriminator").HasValue("a"))
.Map<NewWidgetB>(w => w.Requires("Discriminator).HasValue("b"));
At this point, I have used an integration test and successfully checked that I can save to both tables.
Now, I want to add in the details:
public class FooDetails {
public int Id { get; set; }
public int NewWidgetId { get; set; }
// ...
[ForeignKey(nameof(NewWidgetId))]
public NewWidgetBase NewWidget { get; set; }
}
public class BarDetails {
public int Id { get; set; }
public int NewWidgetId { get; set; }
// ...
[ForeignKey(nameof(NewWidgetId))]
public NewWidgetBase NewWidget { get; set; }
}
I then add those reference properties to my appropriate NewWidget
objects.
public class NewWidgetA {
// ...
public FooDetails Foo { get; set; }
}
public class NewWidgetB {
// ...
public FooDetails Foo { get; set; }
public BarDetails Bar { get; set; }
}
I tried just executing this, assuming that the typical mapping would work, and got the following error:
System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details. ---> System.Data.Entity.Core.UpdateException: Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.
With that, I understood that it doesn't have the correct Relationship directions and keys mapped. So I went to explicitly set it within the DbContext again:
modelBuilder.Entity<NewWidgetA>()
.HasRequired(w => w.Foo)
.WithRequiredDependent();
However, that gives me the error:
System.InvalidOperationException: A dependent property in a ReferentialConstraint is mapped to a store-generated column. Column: 'WidgetId'.
I looked at a "some other" "questions", and none of those answers helped me.
As a last ditch effort, I tried using the overload for .WithRequiredDependent()
which takes a Func. However, because it isn't the exact same type as I'm mapping because I have the property as the abstract base, it complains. Therefore, I try casting it like so:
modelBuilder.Entity<NewWidgetA>()
.HasRequired(w => w.Foo)
.WithRequiredDependent(f => (NewWidgetA)f.Widget);
modelBuilder.Entity<NewWidgetB>()
.HasRequired(w => w.Foo)
.WithRequiredDependent(f => (NewWidgetB).Widget);
modelBuilder.Entity<NewWidgetB>()
.HasRequired(w => w.Bar)
.WithRequiredDependent(b => (NewWidgetB).Widget);
However, this also gives an error:
The ForeignKeyAttribute on property 'Widget' on type '...Foo' is not valid. The foreign key name 'WidgetId' was not found on the dependent type 'NewWidgetA'. The Name value should be a comma separated list of foreign key property names.
This is leading me to believe that I'm unable to do what I want to do with having abstract properties. Is there a way to map this relationship that I'm missing? I don't want to have a specific reference property for each as I know there are more types coming within a month or two, and the list of properties will get unwieldy.
Upvotes: 2
Views: 221
Reputation: 205759
It's possible, but only with unidirectional (with navigation property only at Widget
side) one-to-one Shared Primary Key Association, where the Widget
side is the principal and the Details
side is the dependent.
Start by removing the navigation and FK properties from Details
entities:
public class FooDetails {
public int Id { get; set; }
// ...
}
public class BarDetails {
public int Id { get; set; }
// ...
}
and use the following fluent configuration:
modelBuilder.Entity<NewWidgetA>()
.HasRequired(w => w.Foo)
.WithRequiredPrincipal();
modelBuilder.Entity<NewWidgetB>()
.HasRequired(w => w.Foo)
.WithRequiredPrincipal();
modelBuilder.Entity<NewWidgetB>()
.HasRequired(w => w.Bar)
.WithRequiredPrincipal();
Note the WithRequiredPrincipal()
call. It's telling EF that (1) the Widget
is the principal and (2) there is no navigation property from Details
to Widget
.
The resulting database schema is something like this:
CreateTable(
"dbo.BaseWidget",
c => new
{
Id = c.Int(nullable: false, identity: true),
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.ExistingWidget",
c => new
{
Id = c.Int(nullable: false),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.BaseWidget", t => t.Id)
.Index(t => t.Id);
CreateTable(
"dbo.NewWidgetBase",
c => new
{
Id = c.Int(nullable: false),
EmployeeNumber = c.Int(nullable: false),
EffectiveDate = c.DateTime(nullable: false),
Discriminator = c.String(nullable: false, maxLength: 128),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.BaseWidget", t => t.Id)
.Index(t => t.Id);
CreateTable(
"dbo.FooDetails",
c => new
{
Id = c.Int(nullable: false),
Data = c.String(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.NewWidgetBase", t => t.Id)
.Index(t => t.Id);
CreateTable(
"dbo.BarDetails",
c => new
{
Id = c.Int(nullable: false),
Data = c.String(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.NewWidgetBase", t => t.Id)
.Index(t => t.Id);
Upvotes: 2