asp.net-mvcentity-frameworkone-to-manyef-fluent-apifluent-interface

Implementing a One-to-Zero-or-One Relationship and a One-to-Many Relationship of the Same Class


I am having trouble mapping the following classes.

I want MainAboutPage to be optional (one-to-zero-or-one) and AboutSubPages is obviously one-to-many.

Ideally I want to keep the WebsiteId property on the WebsitePage class.

public class Website
{
    public int Id { get; set; }

    public virtual WebsitePage MainAboutPage { get; set; }

    public ICollection<WebsitePage> AboutSubPages { get; set; }

}


public class WebsitePage
{
    public int Id { get; set; }

    public int WebsiteId { get; set; }

    public virtual Website Website { get; set; }
}

When I use no fluent mapping I get

Unable to determine the principal end of the relationship. Multiple added entities may have the same primary key.


When I use this fluent mapping:

        modelBuilder.Entity<Wesbite>()
            .HasMany(x => x.AboutSubPages)
            .WithRequired(x => x.Website)
            .HasForeignKey(x => x.WebsiteId);

I get:

Unable to determine the principal end of the 'Wesbite_AboutSubPages' relationship. Multiple added entities may have the same primary key.


And when I use this fluent mapping:

        modelBuilder.Entity<Website>()
           .HasOptional(x => x.MainAboutPage)
           .WithRequired();

        modelBuilder.Entity<Wesbite>()
            .HasMany(x => x.AboutSubPages)
            .WithRequired(x => x.Website)
            .HasForeignKey(x => x.WebsiteId);

I get:

Unable to determine the principal end of the 'Website_MainAboutPage' relationship. Multiple added entities may have the same primary key.


And when I use this fluent mapping:

        modelBuilder.Entity<Website>()
           .HasOptional(x => x.MainAboutPage)
           .WithRequired(x => x.Website);

        modelBuilder.Entity<Wesbite>()
            .HasMany(x => x.AboutSubPages)
            .WithRequired(x => x.Website)
            .HasForeignKey(x => x.WebsiteId);

I get:

Wesbite_MainAboutPage_Target: : Multiplicity is not valid in Role 'Wesbite_MainAboutPage_Target' in relationship 'Website_MainAboutPage'. Because the Dependent Role properties are not the key properties, the upper bound of the multiplicity of the Dependent Role must be '*'.


I have been endlessly reading the configuration samples from MS: https://www.entityframeworktutorial.net/code-first/configure-one-to-one-relationship-in-code-first.aspx and https://www.entityframeworktutorial.net/code-first/configure-one-to-many-relationship-in-code-first.aspx

My brain is pickled, please excuse me if I am missing something obvious. I'd really appreciate some pointers toward getting this set up as I'd like.

Thanks in advance.


Solution

  • I believe the issue will lie in that you don't have enough information for EF to differentiate between the AboutSubPages and the MainAboutPage reference relative to the Website. To have a 1..0/1 relationship for MainAboutPage on the Website, you would need a MainAboutPageId declared in the Website table.

    modelBuilder.Entity<Website>()
       .HasOptional(x => x.MainAboutPage)
       .WithRequired(x => x.Website)
       .HasForeignKey(x => x.MainAboutPageId);
    

    Or you can elect to use a Shadow Property (EF Core) or Map.MapKey (EF6) to map the relationship without the FK exposed in the entity. (Recommended)

    The caveat of having both a 1..0/1 plus 1..many of entity relationship to the same related entities is that there is no way to enforce that the MainAboutPage actually belongs to the sub collection. Because the 1..0/1 relies on a FK from web page to the sub page, nothing enforces that sub page has to point back to the same website. EF and Database are perfectly happy to have WebSite ID #1 point to a sub page with a WebSite ID #2.

    A better approach may be to look at just maintaining an AboutSubPages collection and adding a PageOrder numeric unique index to the SubPage entity. The "main" sub page would be the one with the lowest PageOrder for example.

    I.e. to select a web site details and it's "main" about page:

    var websites = context.Websites
        .Select(x => new WebsiteSummaryViewModel
        {
            Name = x.Name,
            AboutPageURL = x.AboutSubPages
                .OrderBy(a => a.PageOrder)
                .Select(a => a.Url)
                .FirstOrDefault()
        }).ToList();
    

    ... as an example. This ensures that we can access a main page while ensuring that the only pages a website considers are ones assigned to it. It is possible to set up an unmapped property on the Website entity to return the "MainAboutPage" from the embedded collection, however I don't recommend using unmapped properties as they can easily slip into Linq expressions and EF will either throw an exception or perform a premature execution (EF Core) to address them.