Shadow Properties to access related entity with Table-Per-Hierarchy inheritance model

asp.net-core-mvc c# entity-framework-core linq navigation-properties

Question

I'm building a menu system for a website in ASP.Net Core. Let's assume I have a couple of database tables, one for Pages and one for Articles, although it only really matters that they are different entities. Each of them have a Name and Permalink property.

In my menu, which I want to also store in the database, I want to refer to the Name and Permalink of each entity. I have devised a simple menu class/model structure as follows:

Abstract MenuItem

public abstract class MenuItem
{
    [Key]
    public int MenuItemId { get; set; }
    public int MenuPosition { get; set; }
    public abstract string Name { get; }
    public abstract string Permalink { get; }
}

Concrete ArticleMenuItem

public class ArticleMenuItem : MenuItem
{
    public ArticleMenuItem() {}
    public ArticleMenuItem(Article article)
    {
        Article = article;
    }

    public string Name => Article.Name;
    public string Permalink => Article.Permalink;

    [Required]
    public int ArticleId { get; set; }
    [ForeignKey("ArticleId")]
    public Article Article { get; set; }
}

Concrete PageMenuItem

public class PageMenuItem : MenuItem
{
    public PageMenuItem() {}
    public PageMenuItem(Page page)
    {
        Page = page;
    }

    public string Name => Page.Name;
    public string Permalink => Page.Permalink;

    [Required]
    public int PageId { get; set; }
    [ForeignKey("PageId")]
    public Page Page{ get; set; }
}

I then override onModelCreating(ModelBuilder modelBuilder) for the relevant DbContext as I don't want to make the individual DbSet<T>'s available:

modelBuilder.Entity<PageMenuItem>();
modelBuilder.Entity<ArticleMenuItem>();

As well as add the relevant DbSet<T> for the menu:

public virtual DbSet<MenuItem> MenuItems { get; set; }

Add a couple of sample records to the database when the app loads (assume I've got some articles and pages initialised too):

List<MenuItem> items = new List<MenuItem>()
{
    new PageMenuItem(pages[0]) { MenuPosition = 1 },
    new ArticleMenuItem(articles[0]) { MenuPosition = 2 }
};
items.ForEach(item => context.MenuItems.Add(item));

A simple repository method to get the menu items from the database:

public IEnumerable<MenuItem> GetAllMenuItems() => _context.MenuItems;

With all this in place I was hoping that I could get the Name and Permalink for each item as follows (in a view, for instance):

@foreach (MenuItem item in Model)
{
    <a href="@item.Permalink">@item.Name</a>
}

Sadly, this results in a null object exception, and then I remembered EF Core doesn't support lazy loading. So I want to eagerly load the shadow properties, specifically the related entities, when I get the menu items in the repository.

There are two approaches to accessing shadow properties. The first approach I took updating my repository method looked like this:

public IEnumerable<MenuItem> GetAllMenuItems() => _context.MenuItems
    .Include(item => context.Entry(item).Property("Page").CurrentValue)
    .Include(item => context.Entry(item).Property("Article").CurrentValue)

This results in:

InvalidCastException: Unable to cast object of type 'System.Linq.Expressions.InstanceMethodCallExpression1' to type 'System.Linq.Expressions.MemberExpression'.

Casting to (Page) and (Aticle) respectively results in:

InvalidOperationException: The property expression 'item => Convert(value(InfoSecipediaWeb.Infrastructure.EntityFramework.ApplicationDbContext).Entry(item).Property("Page").CurrentValue)' is not valid. The expression should represent a property access: 't => t.MyProperty'.

The second method for accessing shadow properties only seems to enable accessing a single property value:

public static TProperty Property<TProperty>([NotNullAttribute] object entity, [NotNullAttribute][NotParameterized] string propertyName);

However, giving it a try:

public IEnumerable<MenuItem> GetAllMenuItems() => _context.MenuItems
    .Include(item => EF.Property<Page>(item, "Page"))
    .Include(item => EF.Property<Article>(item, "Article"));

Results in:

InvalidOperationException: The property expression 'item => Property(item, "Page")' is not valid. The expression should represent a property access: 't => t.MyProperty'.

I'd like to know whether it is possible to use shadow properties for navigation with an inheritance model? If so, how do I include the related entities so that it is accessible in my concrete MenuItem classes? e.g. for public string Name => Page.Name.

1
1
1/17/2017 6:01:04 PM

Accepted Answer

Unfortunately currently there is no syntax for eager loading derived class properties (note that they are different from shadow properties). This along with the lack of lazy loading leaves the explicit loading to be the only option. See for instance ef-core load collection property of nested tph inherited member how you can use it for a single item, for collection of items I'm afraid you have to materialize the result into a list, then using explicit loading of the concrete types and rely on EF navigation property fix up.

For your example it could be something like this:

public IEnumerable<MenuItem> GetAllMenuItems()
{
    var menuItems = _context.MenuItems.ToList();
    _context.MenuItems.OfType<ArticleMenuItem>().Include(e => e.Article).Load();
    _context.MenuItems.OfType<PageMenuItem>().Include(e => e.Page).Load();
    return menuItems;
}

Another workaround (did I say only one) is to use manual union query, which basically kills the TPH idea:

public IEnumerable<MenuItem> GetAllMenuItems() =>
    _context.MenuItems.OfType<ArticleMenuItem>().Include(e => e.Article)
    .AsEnumerable() // to avoid runtime exception (EF Core bug)
    .Concat<MenuItem>(
    _context.MenuItems.OfType<PageMenuItem>().Include(e => e.Page));
3
5/23/2017 11:52:55 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow