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