5

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...

ERD

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.

ERD2

Community
  • 1
  • 1
philreed
  • 2,497
  • 5
  • 26
  • 55
  • 1
    This is a good reference too: http://weblogs.asp.net/manavi/archive/2010/12/24/inheritance-mapping-strategies-with-entity-framework-code-first-ctp5-part-1-table-per-hierarchy-tph.aspx – Colin Jan 13 '14 at 13:48
  • @Colin Thanks, yea I found too that just after writing the question. I'm working through it now. – philreed Jan 13 '14 at 14:27
  • http://stackoverflow.com/questions/7079378/how-to-filter-nested-collection-entity-framework-objects another stack question more or less what you want.. No clean solution though. I hope things like this will be included with the next EF version... – Tsasken Jan 13 '14 at 14:34
  • @Tsasken Thank you, I'll have a read shortly. – philreed Jan 13 '14 at 14:36
  • One this is you need to map the base type (`Area`). This can be a mapping resembling one of a subtype. Then in subtype mappings you _only_ map things specific to a subtype. Further, as everything will be in one table, `ParentId` must be nullable. – Gert Arnold Jan 13 '14 at 22:10
  • @GertArnold Thanks for your feedback. I do not understand why `ParentId` must be nullable? It is a required field of both subtypes, do you have more information regarding this? I have adjusted my mapping as you have suggested and the `ParentId` error is what I need to resolve next. – philreed Jan 14 '14 at 09:58
  • Not all records will have a ParentId, will they? Some Areas will be top level. – Gert Arnold Jan 14 '14 at 10:01
  • @GertArnold Yes, all records will have a `ParentId`. `SubArea` will have a parent of type `SubArea` or `SiteArea`(in this case, I define a relationship to the base class `Area`). `SiteArea` will have a parent of type `Building`. I have removed the null constraint in my DB anyway to try and it had no effect. – philreed Jan 14 '14 at 10:12
  • @GertArnold I have updated my question to include an image of the model I'm trying to create. – philreed Jan 14 '14 at 10:22
  • This user seems to have posted a similar question regarding sharing columns on sub-classes (as I am with `ParentId`). It looks like what Im trying to achine isn't possible in EF5. It does however suggest that TPT might offer a solution which fits my requirements. http://stackoverflow.com/questions/4389227/how-to-share-common-column-names-in-a-hierarchy-per-table-mapping – philreed Jan 14 '14 at 11:25
  • And this is the problem I'm hitting: http://entityframework.codeplex.com/workitem/583 – philreed Jan 14 '14 at 11:31
  • @philreed: expect to get trouble, if you are planning on using cascading deletes. EF doesn't like the possibility of multiple (possible) parents... – k.c. Jul 01 '14 at 09:29

1 Answers1

0

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.