In this lesson, we'll learn how to create database relationships using EF Core navigation properties. We'll add a one-to-many relationship between Items
and Categories
so that each Item
belongs to a specific Category
.
Before we get started, let's review one-to-many relationships. Let's say we have two classes, Team
and Player
. We can conceptualize a one-to-many relationship between Team
and Player
by recognizing the following:
Team
has many Player
s in it.sPlayer
may only belong to one Team
at a time. We would integrate this one-to-many relationship into a database by making sure that each Player
entry has a TeamId
to denote which specific Team
they belong to, "linking" the tables together. In this case, a Player
entry can only have one TeamId
value, because a Player
can only belong to one team.
First, let's update our to_do_list_with_ef_core
database to include a categories
table. We'll also make updates so that we can establish a relationship between the categories
and items
tables.
Create a categories
table.
CategoryId
as a column. It should be an int
, primary key
, non null
, and auto incrementing
.Name
as a column. It should be a Varchar(255)
.Add a CategoryId
column of type int
in the items
table. Set the Default/Expression to 0, to avoid Null
errors.
CategoryId
column value to 0 for all items.Don't forget to hit Apply and confirm that the changes actually happen.
First, we need to add a Categories
DbSet to ToDoListContext.cs
:
using Microsoft.EntityFrameworkCore;
namespace ToDoList.Models
{
public class ToDoListContext : DbContext
{
public DbSet<Category> Categories { get; set; }
public DbSet<Item> Items { get; set; }
public ToDoListContext(DbContextOptions options) : base(options) { }
}
}
Category
Class and Creating a Navigation PropertyNext, we'll completely update the old version of our Category
class (found in Models/Category.cs
):
using System.Collections.Generic;
namespace ToDoList.Models
{
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public List<Item> Items { get; set; }
}
}
Let's walk through this file together:
The Category
class includes a property for its ID and name.
The Category
class includes a public property called Items
that will hold a List
of all Item
s that belong to a category.
For EF Core, Items
is a navigation property, specifically a "collection navigation property".
A navigation property is a property on one entity (like Category
) that includes a reference to a related entity (like Item
). EF Core uses navigation properties to recognize when there is a relationship between two entities.
In this case, EF Core sees that the Items
property has the type List<Item>
which references another entity Item
within our project, and because of that is able to understand that there is a relationship between Category
and Item
.
The Items
property is more specifically categorized as a collection navigation property because it contains multiple entities. In this case, we have a collection (List<>
) of multiple Item
objects.
Notably, navigation properties are never saved in the database. Instead, they are populated in our projects by EF Core from the data in the database.
Next, we need to update our CategoriesController
and replace the CRUD actions with our new Entity-backed ones. This controller will look like the ItemsController
and views we completed in the previous lessons. Because we've already covered how to set up CRUD functionality, it's your task now to build out the controller and its corresponding views on your own.
We will further configure the CategoriesController
and views later in this lesson, so pause now to create these files.
Let's add a link in the homepage (Home/Index.cshtml
) to go to our categories index view.
...
<p>@Html.ActionLink("See all categories", "Index", "Categories")</p>
Let's make sure to also add a link to the homepage in the Categories/Index.cshtml
and Items/Index.cshtml
views:
<p>@Html.ActionLink("Home", "Index", "Home")</p>
Now that we've created CRUD functionality and the respective views for categories, let's go ahead and implement the Category
to Item
relationship into our application.
Item
Class to Include a Navigation Property for Category
Finally, we need to update the Item
class to set up its new relationship to Category
:
namespace ToDoList.Models
{
...
{
...
public int CategoryId { get; set; }
public Category Category { get; set; }
}
}
Since each Item
will be associated with a Category
, the Item
class now has a CategoryId
property. The CategoryId
property is the foreign key in our items
database table that connects each Item
with a Category
.
We also include a new property called Category
set to the Category
object that the Item
belongs to. This is the navigation property in our Item
model that creates the one-to-many relationship between Category
and Item
.
More specifically, the Category
property is called a reference navigation property, because it holds a reference to a single related entity. Now with the Category
property in place, we can fetch the actual Category
object when we fetch the Item
object from the database. We'll see exactly how to do this next!
ItemsController
and Index
View to Display the Category
Let's update our ItemsController
so that whenever an Item
is loaded, its corresponding Category
is available as well. We can do so by using Entity's built-in Include()
method. We'll make a small update to the Index()
action in our ItemsController
. Note that we're including a new using
directive for Microsoft.EntityFrameworkCore
so that we can use the Include()
method.
using Microsoft.EntityFrameworkCore;
...
public ActionResult Index()
{
List<Item> model = _db.Items
.Include(item => item.Category)
.ToList();
return View(model);
}
...
With this update, we're having EF Core do the following: for each Item
in the database, include the Category
it belongs to, and then put all the Item
s into list.
When we tell EF Core to .Include(item => item.Category)
, we're telling it to go into the categories
table and retrieve the Category
object with all of its data and then populate the Item.Category
property with that data.
If we don't explicitly tell EF Core to include the navigation property Category
, it won't. We'll still get the Item.ItemId
, Item.Description
, and Item.CategoryId
properties, but the navigation property Category
will be empty.
Next, we'll update the Item Index
view to display the category that each item belongs to. We'll add the name of the Category
next to each Item
in the view.
@{
Layout = "_Layout";
}
@using ToDoList.Models;
@model List<ToDoList.Models.Item>;
<h1>Items</h1>
@if (@Model.Count == 0)
{
<h3>No items have been added yet!</h3>
}
else
{
<ul>
@foreach (Item item in Model)
{
<li>@Html.ActionLink($"{item.Description}", "Details", new { id = item.ItemId }) | @item.Category.Name</li>
}
</ul>
}
<p>@Html.ActionLink("Add new item", "Create")</p>
Our index view will now show both an Item
and the name of its related Category
!
CategoriesController
and Details
View to Display the Items
that Belong to Each Category
Next, let's use the Include()
method to include and display the items that belong to each Category
. We'll display this information on the Category
details page.
First, let's update the Details action in the CategoriesController.cs
:
using Microsoft.EntityFrameworkCore;
...
public ActionResult Details(int id)
{
Category thisCategory = _db.Categories
.Include(category => category.Items)
.FirstOrDefault(category => category.CategoryId == id);
return View(thisCategory);
}
...
Here we state that we want to include the Items
property, which tells EF Core to fetch every Item
object belonging to the Category
.
Just like before, if we don't explicitly tell EF Core to include the data for the navigation property Items
with the code .Include(category => category.Items)
, it won't gather that data. However, we'll still get the Category.CategoryId
and the Category.Name
information.
Next, let's update the Category
details view:
@{
Layout = "_Layout";
}
@model ToDoList.Models.Category
@using ToDoList.Models
<h2>Category Details</h2>
<hr />
<h3>@Html.DisplayNameFor(model => model.Name): @Html.DisplayFor(model => model.Name)<h3>
@if (@Model.Items.Count == 0)
{
<h3>No items have been added to this category yet!</h3>
}
else
{
<h3>Items:</h3>
<ul>
@foreach (Item item in Model.Items)
{
<li>@Html.ActionLink($"{item.Description}", "Details", "Items", new { id = item.ItemId })</li>
}
</ul>
}
<p>@Html.ActionLink("Edit Category", "Edit", new { id = Model.CategoryId })</p>
<p>@Html.ActionLink("Delete Category", "Delete", new { id = Model.CategoryId })</p>
<p>@Html.ActionLink("Back to list", "Index")</p>
Much of this code should look familiar, like using an if statement and looping through a list of objects. However, there's a two new changes to note in the update we've made:
@using
directive for ToDoList.Models
. This enables us to use the Item
model in our view alongside the Category
model which we've specified as the main @model
that we're using in the view.HTML.ActionLink
contains an additional argument in order to route to the ItemsController
: note the 3rd argument of "Items"
. If we don't include this, our ActionLink
will route us to the category's Details
view.At this point, we've set up the database to track a one-to-many relationship between Item
s and Category
s. However, there is still one key thing missing: there's no way for users to actually make the association between an Item
and a Category
in our application! In other words, we've set up the READ functionality for an association in our index view but users can't actually CREATE associations yet. In the next two lessons, we'll update the rest of our methods to add this functionality and learn about a property of Views called ViewBag
.