我正在為ASP.Net Core中的網站構建一個菜單系統。讓我們假設我有幾個數據庫表,一個用於Pages
,一個用於Articles
,儘管它們只是它們是不同的實體才真正重要。它們中的每一個都具有Name
和Permalink
屬性。
在我想要存儲在數據庫中的菜單中,我想引用每個實體的Name
和Permalink
。我設計了一個簡單的菜單類/模型結構如下:
抽象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; }
}
具體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; }
}
具體的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; }
}
然後我為相關的DbContext
重寫onModelCreating(ModelBuilder modelBuilder)
,因為我不想使單獨的DbSet<T>'s
可用:
modelBuilder.Entity<PageMenuItem>();
modelBuilder.Entity<ArticleMenuItem>();
除了為菜單添加相關的DbSet<T>
:
public virtual DbSet<MenuItem> MenuItems { get; set; }
在加載應用程序時,將幾個示例記錄添加到數據庫中(假設我已經初始化了一些文章和頁面):
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));
從數據庫中獲取菜單項的簡單存儲庫方法:
public IEnumerable<MenuItem> GetAllMenuItems() => _context.MenuItems;
有了這一切,我希望我可以獲得每個項目的Name
和Permalink
,如下所示(例如,在視圖中):
@foreach (MenuItem item in Model)
{
<a href="@item.Permalink">@item.Name</a>
}
遺憾的是,這會導致null object exception
,然後我記得EF Core不支持延遲加載。因此,當我獲取存儲庫中的菜單項時,我想急切地加載陰影屬性,特別是相關實體。
訪問陰影屬性有兩種方法 。我更新我的存儲庫方法的第一種方法如下所示:
public IEnumerable<MenuItem> GetAllMenuItems() => _context.MenuItems
.Include(item => context.Entry(item).Property("Page").CurrentValue)
.Include(item => context.Entry(item).Property("Article").CurrentValue)
這導致:
InvalidCastException:無法將類型為“System.Linq.Expressions.InstanceMethodCallExpression1”的對象強制轉換為“System.Linq.Expressions.MemberExpression”。
分別轉換為(Page)
和(Aticle)
導致:
InvalidOperationException:屬性表達式'item => Convert(value(InfoSecipediaWeb.Infrastructure.EntityFramework.ApplicationDbContext).Entry(item).Property(“Page”)。CurrentValue)'無效。表達式應表示屬性訪問:'t => t.MyProperty'。
訪問陰影屬性的第二種方法似乎只能訪問單個屬性值:
public static TProperty Property<TProperty>([NotNullAttribute] object entity, [NotNullAttribute][NotParameterized] string propertyName);
但是,嘗試一下:
public IEnumerable<MenuItem> GetAllMenuItems() => _context.MenuItems
.Include(item => EF.Property<Page>(item, "Page"))
.Include(item => EF.Property<Article>(item, "Article"));
結果是:
InvalidOperationException:屬性表達式'item => Property(item,“Page”)'無效。表達式應表示屬性訪問:'t => t.MyProperty'。
我想知道是否可以使用陰影屬性進行導航模型的導航?如果是這樣,我如何包含相關實體,以便在我的具體MenuItem
類中可以訪問它?例如,對於public string Name => Page.Name
。
不幸的是,目前沒有預先加載派生類屬性的語法(請注意它們與陰影屬性不同)。這與缺乏延遲加載一起使得顯式加載成為唯一的選擇。例如,參見嵌套tph繼承成員的ef-core load collection屬性如何將它用於單個項目,對於項目集合我恐怕必須將結果具體化為列表,然後使用顯式加載具體類型並依靠EF導航屬性修復。
對於你的例子,它可能是這樣的:
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;
}
另一個解決方法(我只說一個)是使用手動聯合查詢,它基本上殺死了TPH的想法:
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));