Introduction
This is Part III of a multi-part article demonstrates the mapping of C# enum values to string values in database tables via EntityFramework Core 2.1 (EF). It addresses the mapping of enum values in one-to-many and many-to-many relationships with application entities. It does this in the context of an ASP.NET Core Razor Page application.
EF is an Object-Relational Mapper (ORM). In an application such as this sample, there are two "worlds". One is the object world that exists as an object model in C#. The other is the relational world that exists in a relational database, like Microsoft SQL Server. These two worlds are not consistent with each other. The function of an ORM, like EntityFramework, is the bridge between these two worlds and facilitates the transfer of data between them.
Part I. Setup the Entity Framework data context and the initial Customers Razor Pages
Part II. Completed CRUD functions for Customers
In Part III. We will create Project and ProjectState entities and implement a one-to-many relationship between ProjectState and Projects as follows:
- Add the
Project, ProjectState and ProjectStateDescription entities. - Add an EF migration to create and configure the
Projects and ProjectStateDescriptions tables in the database. - Demonstrate the conversion between
enum values in the object model entities and the string values in the Projects and ProjectStateDescriptions database tables. - Scaffold, implement and test
Project CRUD pages, CustomerProjects.cshtml, CustomerProjectCreate.cshtml, CustomerProjectDetails.cshtml and CustomerProjectDelete.cshtml Razor pages that include the ProjectState feature.
Part IV. Add Skill entities (Skill enum, SkillTitle and ProjectSkill) and implement a many-to-many relationship between Projects and Skills.
Using the Code
Add Initial Project Processing.
Next, we enable Customer Project processing. The application uses the Customer as a "gateway" entity; everything is reached via the Customer. There is a one-to-many relationship between a Customer and Projects. Therefore, we need to modify the Customer class.
Modified Customer.cs:
using System.Collections.Generic;
namespace QuantumWeb.Model
{
public class Customer
{
#region Constructors
public Customer()
{
}
#endregion // Constructors
public int CustomerId { get; set; }
public string CustomerName { get; set; }
public string CustomerContact { get; set; }
public string CustomerPhone { get; set; }
public string CustomerEmail { get; set; }
#region Navigation Properties
public List<Project> Projects { get; set; }
#endregion // Navigation Properties
}
}
We added a List of Projects (bolded above). Here, we identify certain properties as Navigation Properties. These properties reference other classes/entities so that we can navigate to them in processing. A Customer has zero or more Projects represented in the list of Projects. The initial Project class definition is below.
Initial Project.cs:
namespace QuantumWeb.Model
{
public class Project
{
public int ProjectId { get; set; }
public string ProjectName { get; set; }
#region Navigation Properties
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public ProjectState ProjectStateCode { get; set; }
public ProjectStateDescription ProjectStateDescription { get; set; }
#endregion // Navigation Properties
}
}
In addition to defining the initial Project class, we will also define the ProjectState enum in the Model folder.
ProjectState.cs:
namespace QuantumWeb.Model
{
public enum ProjectState
{
Prospect,
UnderReview,
StartScheduled,
InProgress,
Completed
}
}
This enum specifies the states of the Project workflow.
Prospect. This addresses a prospective Project. This Project might have been presented via a referral or other marketing efforts. No research has been done and the specifications are not known. UnderReview. In this state, the Project requirements, initial budget and schedule are developed. There is no commitment by Quantum or the Customer. StartScheduled. The date that work is to start has been specified and preparation to start work is in progress. InProgress. Actual work has started but is not complete. Completed. Project work is complete.
As previously stated, we have two objectives for this application.
- We should define a short description for each
Project state that will be displayed in the UI to aid in user understanding of the meaning of each state. - Each
enum value is to be stored in the database as a string.
To meet these requirements for the ProjectState enum, we define the ProjectStateDescription class.
ProjectStateDescription.cs:
using System.Collections.Generic;
namespace QuantumWeb.Model
{
public class ProjectStateDescription
{
public ProjectState ProjectStateCode { get; set; }
public string StateDescription { get; set; }
#region Navigation Properties
public List<Project> Projects { get; set; }
#endregion // Navigation Properties
}
}
The ProjectState to Projects one-to-many relationship is enabled through navigation properties. Each Project has one ProjectStateDesciption. Each ProjectStateDescripton has a collection of Projects.
Next, we need to define the EF configuration classes for Project and ProjectStateDescription and include all in the QuantumDbContext class. All of this activity occurs in the Data folder.
Initial ProjectConfiguration.cs:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using QuantumWeb.Model;
namespace QuantumWeb.Data
{
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
public void Configure(EntityTypeBuilder<Project> builder)
{
builder.ToTable("Projects");
builder.HasKey(p => p.ProjectId);
builder.Property(p => p.ProjectId)
.HasColumnType("int");
builder.Property(p => p.ProjectName)
.IsRequired()
.HasColumnType("nvarchar(80)")
.HasMaxLength(80);
builder.Property(p => p.CustomerId)
.HasColumnType("int")
.IsRequired();
builder.HasOne(p => p.Customer)
.WithMany(c => c.Projects)
.HasForeignKey(p => p.CustomerId)
.IsRequired();
builder.Property(p => p.ProjectStateCode)
.HasColumnType("nvarchar(15)")
.HasDefaultValue(ProjectState.Prospect)
.HasConversion(
p => p.ToString(),
p => (ProjectState)Enum.Parse(typeof(ProjectState), p));
builder.HasOne(p => p.ProjectStateDescription)
.WithMany(pd => pd.Projects)
.HasForeignKey(p => p.ProjectStateCode);
}
}
}
Take a look at the extracted lines below:
builder.HasOne(p => p.Customer)
.WithMany(c => c.Projects)
.HasForeignKey(p => p.CustomerId)
.IsRequired();
An interpretation of these lines is, "Each Project has one Customer with many Projects. Each Project maps to a Projects table in the database with a foreign key, CustomerId, and is required. Thus, the Customer-Project relationship is one-to-many.
The one-to-many ProjectStateDescription-Project relationship is configured by:
builder.HasOne(p => p.ProjectStateDescription)
.WithMany(pd => pd.Projects)
.HasForeignKey(p => p.ProjectStateCode);
Next, we look at the way the configuration of enum value to database string columns is handled.
builder.Property(p => p.ProjectStateCode)
.HasColumnType("nvarchar(15)")
.HasDefaultValue(ProjectState.Prospect)
.HasConversion(
p => p.ToString(),
p => (ProjectState)Enum.Parse(typeof(ProjectState), p));
These lines first configure an nvarchar(15) column in the Projects table named, ProjectStateCode, with a default value taken from ProjectState.Prospect. Next, the conversion between the ProjectState values and the string value is defined. When moving values from the ProjectState enum to the Projects table, the values are converted using the ToString() function. When converting the other way, the string value in the table is parsed to an enum value. The same scheme is used throughout to convert between enum values and string values in database columns.
The ProjectStateDescriptionConfiguration class is shown below.
ProjectStateDescriptionConfiguration.cs:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using QuantumWeb.Model;
namespace QuantumWeb.Data
{
public class ProjectStateDescriptionConfiguration :
IEntityTypeConfiguration<ProjectStateDescription>
{
public void Configure(EntityTypeBuilder<ProjectStateDescription> builder)
{
builder.ToTable("ProjectStateDescriptions");
builder.HasKey(p => p.ProjectStateCode);
builder.Property(p => p.ProjectStateCode)
.HasColumnType("nvarchar(15)")
.HasConversion(
p => p.ToString(),
p => (ProjectState)Enum.Parse(typeof(ProjectState), p));
builder.Property(p => p.StateDescription)
.IsRequired()
.HasColumnType("nvarchar(80)")
.HasMaxLength(80);
}
}
}
Now, we update the QuantumDbContext class.
Second update to QuantumDbContext.cs:
using Microsoft.EntityFrameworkCore;
using QuantumWeb.Model;
namespace QuantumWeb.Data
{
public class QuantumDbContext : DbContext
{
public QuantumDbContext (DbContextOptions<QuantumDbContext> options)
: base(options)
{
}
#region DbSets
public DbSet<Customer> Customers { get; set; }
public DbSet<Project> Projects { get; set; }
public DbSet<ProjectStateDescription> ProjectStateDescriptions { get; set; }
#endregion // DbSets
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new CustomerConfiguration());
modelBuilder.ApplyConfiguration(new ProjectConfiguration());
modelBuilder.ApplyConfiguration(new ProjectStateDescriptionConfiguration());
}
}
}
The added lines are shown in bold. Now add an EF migration for the Project and ProjectState entities.
Add-Migration Added-Project-ProjectState
Generated ~\Migrations\20181021203503_Added-Project-ProjectState.cs:
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace QuantumWeb.Migrations
{
public partial class AddedProjectProjectState : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ProjectStateDescriptions",
columns: table => new
{
ProjectStateCode =
table.Column<string>(type: "nvarchar(15)", nullable: false),
StateDescription =
table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProjectStateDescriptions", x => x.ProjectStateCode);
});
migrationBuilder.CreateTable(
name: "Projects",
columns: table => new
{
ProjectId = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
ProjectName = table.Column<string>(type: "nvarchar(80)",
maxLength: 80, nullable: false),
CustomerId = table.Column<int>(type: "int", nullable: false),
ProjectStateCode = table.Column<string>
(type: "nvarchar(15)", nullable: false, defaultValue: "Prospect")
},
constraints: table =>
{
table.PrimaryKey("PK_Projects", x => x.ProjectId);
table.ForeignKey(
name: "FK_Projects_Customers_CustomerId",
column: x => x.CustomerId,
principalTable: "Customers",
principalColumn: "CustomerId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Projects_ProjectStateDescriptions_ProjectStateCode",
column: x => x.ProjectStateCode,
principalTable: "ProjectStateDescriptions",
principalColumn: "ProjectStateCode",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Projects_CustomerId",
table: "Projects",
column: "CustomerId");
migrationBuilder.CreateIndex(
name: "IX_Projects_ProjectStateCode",
table: "Projects",
column: "ProjectStateCode");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Projects");
migrationBuilder.DropTable(
name: "ProjectStateDescriptions");
}
}
}
After the Update-Database command, the database diagram from SQL Server Management Studio (SSMS) is shown below.
QuantumDbContext Database Diagram with Customer-Project-ProjectState Tables:

Modify Razor Pages for Project and ProjectState.
We will need to add a number of custom Customer Razor Pages to the application for the Projects. First, we need to add a link to the Customer/Index page for CustomerProjects.
Add CustomerProjects link to Pages\Customers\Index.cshtml:
@page
@model QuantumWeb.Pages.Customers.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
<!--
</p>
<!--
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Customer[0].CustomerName)
</th>
<th>
@Html.DisplayNameFor(model => model.Customer[0].CustomerContact)
</th>
<th>
@Html.DisplayNameFor(model => model.Customer[0].CustomerPhone)
</th>
<th>
@Html.DisplayNameFor(model => model.Customer[0].CustomerEmail)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Customer) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.CustomerName)
</td>
<td>
@Html.DisplayFor(modelItem => item.CustomerContact)
</td>
<td>
@Html.DisplayFor(modelItem => item.CustomerPhone)
</td>
<td>
@Html.DisplayFor(modelItem => item.CustomerEmail)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.CustomerId">Edit</a> |
<!--
<a asp-page="./Details" asp-route-id="@item.CustomerId">Details</a> |
<!--
<a asp-page="./CustomerProjects" asp-route-id="@item.CustomerId">Projects</a> |
<!--
<a asp-page="./Delete" asp-route-id="@item.CustomerId">Delete</a>
<!--
</td>
</tr>
}
</tbody>
</table>
We will scaffold several custom Customers Razor Pages as follows.
Custom Scaffold for Customers Razor Pages:

Scaffold Customers/CustomerProjects Razor Page:

Clicking "Add" will produce shell files for the CustomerProjects Index page.
Generated ~Pages\Customers\CustomerProjects.cshtml
@page
@model QuantumWeb.Pages.Customers.CustomerProjectsModel
@{
ViewData["Title"] = "CustomerProjects";
}
<h2>CustomerProjects</h2>
Generated ~Pages\Customers\CustomerProjects.cshtml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace QuantumWeb.Pages.Customers
{
public class CustomerProjectsModel : PageModel
{
public void OnGet()
{
}
}
}
We will modify these shell files in each case to suit our needs. The modified files for the CustomerProjects Index page are.
Modified ~Pages\Customers\CustomerProjects.cshtml
@page "{id:int?}"
@model QuantumWeb.Pages.Customers.CustomerProjectsModel
@{
ViewData["Title"] = "Customer Projects";
}
<h2>Customer Projects</h2>
<div>
<h4>Customer</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Customer.CustomerId)
</dt>
<dd>
@Html.DisplayFor(model => model.Customer.CustomerId)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Customer.CustomerName)
</dt>
<dd>
@Html.DisplayFor(model => model.Customer.CustomerName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Customer.Projects)
</dt>
<dd>
<table class="table">
<tr>
<th>Project ID</th>
<th>Project Name</th>
<th>Project State</th>
<th></th>
</tr>
@foreach (var item in Model.Customer.Projects)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.ProjectId)
</td>
<td>
@Html.DisplayFor(modelItem => item.ProjectName)
</td>
<td>
@Html.DisplayFor(modelItem => item.ProjectStateCode)
</td>
<td>
<a asp-page="./CustomerProjectEdit"
asp-route-id="@item.ProjectId">Edit</a> |
<a asp-page="./CustomerProjectDelete"
asp-route-id="@item.ProjectId">Delete</a>
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="CustomerProjectCreate" asp-route-id="@Model.Customer.CustomerId">
Create New Project</a> |
<a asp-page="./Index">Back to List</a>
</div>
The "{id:int?}" indicates an integer parameter, id, is needed or the request for the page will return an HTTP 401 (Page not found) error. In this case, this is the identifier (CustomerId) of the target Customer. Also, notice the link referencing the CustomerProjectCreate page.
<a asp-page="CustomerProjectCreate" asp-route-id="@Model.Customer.CustomerId">Create New Project</a> |
This will take us to the CustomerProjectCreate page, yet to be created, to create a new Project for the referenced Customer.
Modified ~Pages\Customers\CustomerProjects.cshtml.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using QuantumWeb.Data;
using QuantumWeb.Model;
namespace QuantumWeb.Pages.Customers
{
public class CustomerProjectsModel : PageModel
{
private readonly QuantumDbContext _context;
public CustomerProjectsModel(QuantumDbContext context)
{
_context = context;
}
public Customer Customer { get; set; }
public async Task<IActionResult> OnGet(int? id)
{
if (id == null)
{
return NotFound();
}
Customer = await _context.Customers
.Include(c => c.Projects)
.FirstOrDefaultAsync(c => c.CustomerId == id);
if (Customer == null)
{
return NotFound();
}
return Page();
}
}
}
Note here that the OnGet handler has a nullable integer parameter, id, which should be the CustomerId mentioned above.
QuantumWeb Application Customers Page: https//localhost: 44306/Customers with Project links.

Customer Projects Page: https//localhost: 44306/Customers/CustomerProjects/1 (No Projects)

The "Create New Project" link will activate the custom CustomerProjectCreate Razor page. We now scaffold this page.
Scaffold Customers/CustomerProjectCreate Razor Page:

Initial ~Pages\Customers\CustomerProjectCreate.cshtml.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using QuantumWeb.Data;
using QuantumWeb.Model;
namespace QuantumWeb.Pages.Customers
{
public class CustomerProjectCreateModel : PageModel
{
private readonly QuantumDbContext _context;
public CustomerProjectCreateModel(QuantumDbContext context)
{
_context = context;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnGet(int? id)
{
if (id == null)
{
return NotFound();
}
Customer = await _context.Customers
.Include(c => c.Projects)
.FirstOrDefaultAsync(c => c.CustomerId == id);
if (Customer == null)
{
return NotFound();
}
ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions,
"ProjectStateCode", "StateDescription", ProjectState.Prospect);
return Page();
}
[BindProperty]
public Project Project { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
Project.CustomerId = Customer.CustomerId;
_context.Projects.Add(Project);
await _context.SaveChangesAsync();
return RedirectToPage("./CustomerProjects", new { id = Customer.CustomerId });
}
}
}
Please take note of these lines in this code.
[BindProperty]
public Customer Customer { get; set; }
The [BindProperty] binds the Customer instance to the elements of the UI so that their values are preserved between the browser and the Web server. Also, notice that this attribute is applied to Project instance as well.
Customer = await _context.Customers
.Include(c => c.Projects)
.FirstOrDefaultAsync(c => c.CustomerId == id);
This statement executes a query against the database to retrieve the Customer record whose primary key value, CustomerId, matches the input parameter, id, value and its associated Project records, if any. The function of the .Include is to include associated records in the query.
ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions,
"ProjectStateCode", "StateDescription", ProjectState.Prospect);
A ViewData is an un-typed key-value dictionary used to pass values between the CustomerProjectCreateModel class (in the .cshtml.cs file) and the HTML in the .cshtml file. This is similar to passing data from the Controller to the View in the MVC, In using ViewData, the data persists only in the HTTP request. Its members are filled from a query from the ProjectStateDescriptions database table. In this case, the _context.ProjectStateDescriptions is an IEnumerable<ProjectStateDescription> returned from the query. The ProjectStateCode is the primary key in the table and represents the key in the ViewData dictionary. The StateDescription becomes the associated value in the ViewData. dictionary. The ViewData will be used to populate a <select> element in CustomerProjectCreate.cshtml. (See below.) The ProjectState.Prospect is the default selected value from the ProjectState enum for the <select>. You can read more on ViewData at the link, https://www.tektutorialshub.com/viewbag-viewdata-asp-net-core/.
Initial ~Pages\Customers\CustomerProjectCreate.cshtml:
@page
@model QuantumWeb.Pages.Customers.CustomerProjectCreateModel
@{
ViewData["Title"] = "Create Customer Project";
}
<h2>Create Customer Project</h2>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Customer.CustomerId)
</dt>
<dd>
@Html.DisplayFor(model => model.Customer.CustomerId)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Customer.CustomerName)
</dt>
<dd>
@Html.DisplayFor(model => model.Customer.CustomerName)
</dd>
</dl>
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Customer.CustomerId" />
<div class="form-group">
<label asp-for="Project.ProjectName" class="control-label"></label>
<input asp-for="Project.ProjectName" class="form-control">
</div>
<div class="form-group">
<label asp-for="Project.ProjectStateCode" class="control-label"></label>
<select asp-for="Project.ProjectStateCode" class="form-control"
asp-items="ViewBag.ProjectStateCode">
</select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="CustomerProjects" asp-route-id="@Model.Customer.CustomerId">
Back to Customer Projects
</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
The key elements are as follows:
<input type="hidden" asp-for="Customer.CustomerId" />
This hidden <input> captures the target CustomerId so that it is available when the <form> is posted to create the Project.
<select asp-for="Project.ProjectStateCode" class="form-control"
asp-items="ViewBag.ProjectStateCode">
</select>
This <select> element will be displayed as a dropdown in the UI with the values from the ViewData populated in the CustomerProjectCreate.OnGet() method.
Initial ~Pages\Customers\CustomerProjectCreate.cshtml:

This shows the Customers/CustomerProjectCreate page as initially displayed.
CustomerProjectCreate Page with data:

After clicking "Create", we will see:
Customer Projects Page with added Project:

The next two figures show things after other Projects are added for both Customers.
Customer Projects Page with 2 Projects for Mirarex Oil & Gas:

Customer Projects Page with 3 Projects for Polyolefin Processing, Inc.

We can now add another page to edit the Customer Projects, the CustomerProjectEdit page.
Scaffold Customers/CustomerProjectEdit Razor Page

Initial ~Pages\Customers\CustomerProjectEdit.cshtml.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using QuantumWeb.Data;
using QuantumWeb.Model;
namespace QuantumApp.Pages.Customers
{
public class CustomerProjectEditModel : PageModel
{
private readonly QuantumDbContext _context;
public CustomerProjectEditModel(QuantumDbContext context)
{
_context = context;
}
[BindProperty]
public Customer Customer { get; set; }
[BindProperty]
public Project Project { get; set; }
public async Task<IActionResult> OnGet(int? id)
{
if (id == null)
{
return NotFound();
}
Project = await _context.Projects
.Include(p => p.Customer)
.FirstOrDefaultAsync(p => p.ProjectId == id);
if (Project == null)
{
return NotFound();
}
Customer = Project.Customer;
ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions,
"ProjectStateCode", "StateDescription", ProjectState.Prospect);
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (!ModelState.IsValid)
{
return Page();
}
var projectToUpdate = await _context.Projects.FindAsync(id);
if (projectToUpdate == null)
{
return NotFound();
}
projectToUpdate.CustomerId = Customer.CustomerId;
if (await TryUpdateModelAsync<Project>(
projectToUpdate,
"project",
p => p.ProjectName, p => p.ProjectStateCode))
{
await _context.SaveChangesAsync();
return RedirectToPage("./CustomerProjects", new { id = Customer.CustomerId });
}
return Page();
}
}
}
This code has the same artifacts as the CustomerProjectCreate page with regard to the .Include and the ViewData.
Initial ~Pages\Customers\CustomerProjectEdit.cshtml
@page "{id:int?}"
@model QuantumWeb.Pages.Customers.CustomerProjectEditModel
@{
ViewData["Title"] = "Edit Customer Project";
}
<h2>Edit Customer Project</h2>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Customer.CustomerId)
</dt>
<dd>
@Html.DisplayFor(model => model.Customer.CustomerId)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Customer.CustomerName)
</dt>
<dd>
@Html.DisplayFor(model => model.Customer.CustomerName)
</dd>
</dl>
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Customer.CustomerId" />
<div class="form-group">
<label asp-for="Project.ProjectName" class="control-label"></label>
<input asp-for="Project.ProjectName" class="form-control">
</div>
<div class="form-group">
<label asp-for="Project.ProjectStateCode" class="control-label"></label>
<select asp-for="Project.ProjectStateCode" class="form-control"
asp-items="ViewBag.ProjectStateCode">
</select>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="CustomerProjects" asp-route-id="@Model.Customer.CustomerId">
Back to Customer Projects
</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
This page has the same elements as the CustomerProjectCreate page with regard to the hidden <input> for the CustomerId and the <select>.
Customer Projects Page with 2 Projects for Mirarex Oil & Gas - For Edit:

Customer Project Edit Page for Mirarex Oil & Gas, Zolar Pipeline:

Customer Projects Page with 2 Projects for Mirarex Oil & Gas - Project Edited:

The final feature for this part Project deletion via the CustomerProjectDelete page.
Scaffold Customers/CustomerProjectDelete Razor Page:

Initial ~Pages\Customers\CustomerProjectDelete.cshtml.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using QuantumWeb.Data;
using QuantumWeb.Model;
namespace QuantumWeb.Pages.Customers
{
public class CustomerProjectDeleteModel : PageModel
{
private readonly QuantumDbContext _context;
public CustomerProjectDeleteModel(QuantumDbContext context)
{
_context = context;
}
[BindProperty]
public Customer Customer { get; set; }
[BindProperty]
public Project Project { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Project = await _context.Projects
.Include(p => p.Customer)
.FirstOrDefaultAsync(p => p.ProjectId == id);
if (Project == null)
{
return NotFound();
}
Customer = Project.Customer;
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Project = await _context.Projects
.Include(p => p.Customer)
.FirstOrDefaultAsync(p => p.ProjectId == id);
if (Project != null)
{
_context.Projects.Remove(Project);
await _context.SaveChangesAsync();
}
return RedirectToPage("./CustomerProjects", new { id = Project.Customer.CustomerId });
}
}
}
Initial ~Pages\Customers\CustomerProjectDelete.cshtml
@page "{id:int?}"
@model QuantumWeb.Pages.Customers.CustomerProjectDeleteModel
@{
ViewData["Title"] = "Delete Customer Project";
}
<h2>Delete Customer Project</h2>
<h3>Are you sure you want to delete this?</h3>
<div>
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Customer.CustomerName)
</dt>
<dd>
@Html.DisplayFor(model => model.Customer.CustomerName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Project.ProjectId)
</dt>
<dd>
@Html.DisplayFor(model => model.Project.ProjectId)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Project.ProjectName)
</dt>
<dd>
@Html.DisplayFor(model => model.Project.ProjectName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Project.ProjectStateCode)
</dt>
<dd>
@Html.DisplayFor(model => model.Project.ProjectStateCode)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Project.ProjectId" />
<a asp-page="CustomerProjects" asp-route-id="@Model.Customer.CustomerId">
Back to Customer Projects
</a> |
<input type="submit" value="Delete" class="btn btn-default" />
</form>
</div>
Customer Projects Page with 3 Projects for Mirarex Oil & Gas:

Delete Customer Projects Page - Delete Ouachita Shale:

Customer Projects Page with 2 Projects for Mirarex Oil & Gas:

At this point, we can summarize the test data in the following table:
Customers, Projects, ProjectStates
| CustomerId | Customer Name | ProjectId | Project Name | ProjectStateCode | StateDescription |
| 1 | Mirarex Oil & Gas, LLC | 1 | Zolar Pipeline | UnderReview | Project is under review and negotiation |
| 1 | Mirarex Oil & Gas, LLC | 2 | Nelar Ranch Gas Fracturing | Prospect | Prospective or referred project |
| 2 | Polyolefin Processing, Inc. | 3 | Port Gibson Plant Expansion | Prospect | Prospective or referred project |
| 2 | Polyolefin Processing, Inc. | 4 | Jackson Plant Control System Upgrade | Prospect | Prospective or referred project |
| 2 | Polyolefin Processing, Inc. | 5 | Eutaw Plant Shutdown & Maintenance | Prospect | Prospective or referred project |
Summary
We have implemented the ProjectState to Project one-to-many relationship and created ASP.NET Razor Pages to manage it.
Points of Interest
In Part IV of this article, we will add definitions of Skill entities (Skill, SkillTitle and ProjectSkills) and implement a many-to-many relationship between Projects and Skills.