Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / CSS

ASP.NET Core Razor Pages Using EntityFramework Core with Enums as Strings - Part III

5.00/5 (5 votes)
2 Dec 2018CPOL9 min read 17K   251  
Add Project and Project State Processing

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:

C#
using System.Collections.Generic;

namespace QuantumWeb.Model
{
    /// <summary>
    /// Customer Class
    /// </summary>
    public class Customer
    {
        #region Constructors

        /// <summary>
        /// Parameter-less Constructor
        /// </summary>
        /// <remarks>
        /// Required for scaffolding the UI
        /// </remarks>
        public Customer()
        {
        } // end public Customer()

        #endregion // Constructors

        /// <summary>
        /// Customer Identifier, primary key
        /// </summary>
        public int CustomerId { get; set; }
        /// <summary>
        /// Customer Name
        /// </summary>
        public string CustomerName { get; set; }
        /// <summary>
        /// Primary Customer Contact
        /// </summary>
        public string CustomerContact { get; set; }
        /// <summary>
        /// Customer Contact Phone Number
        /// </summary>
        public string CustomerPhone { get; set; }
        /// <summary>
        /// Customer Contact Email Address
        /// </summary>
        public string CustomerEmail { get; set; }

        #region Navigation Properties

        /// <summary>
        /// List of Projects
        /// </summary>
        public List<Project> Projects { get; set; }

        #endregion // Navigation Properties

    } // end public class Customer

} // end namespace QuantumWeb.Model

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:

C#
namespace QuantumWeb.Model
{
    /// <summary>
    /// Project Class
    /// </summary>
    public class Project
    {
        /// <summary>
        /// Project Identifier, primary key
        /// </summary>
        public int ProjectId { get; set; }
        /// <summary>
        /// Project Name
        /// </summary>
        public string ProjectName { get; set; }

        #region Navigation Properties

        /// <summary>
        /// Customer Identifier
        /// </summary>
        public int CustomerId { get; set; }

        /// <summary>
        /// Customer
        /// </summary>
        /// <remarks>
        /// Every Project has a Customer
        /// </remarks>
        public Customer Customer { get; set; }

        /// <summary>
        /// Project Status Code
        /// </summary>
        public ProjectState ProjectStateCode { get; set; }

        /// <summary>
        /// ProjectStateDescription Reference
        /// </summary>
        public ProjectStateDescription ProjectStateDescription { get; set; }

        #endregion // Navigation Properties

    } // end public class Project

} // end namespace QuantumApp.Model

In addition to defining the initial Project class, we will also define the ProjectState enum in the Model folder.

ProjectState.cs:

C#
namespace QuantumWeb.Model
{   
    /// <summary>
    /// Project State Enumeration
    /// </summary>
    public enum ProjectState
    {
        Prospect,
        UnderReview,
        StartScheduled,
        InProgress,
        Completed
    } // end public enum ProjectState

} // end namespace QuantumWeb.Model

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.

  1. 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.
  2. 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:

C#
using System.Collections.Generic;

namespace QuantumWeb.Model
{
    /// <summary>
    /// Project State Description Class
    /// </summary>
    public class ProjectStateDescription
    {
        /// <summary>
        /// ProjectState Code
        /// </summary>
        public ProjectState ProjectStateCode { get; set; }

        /// <summary>
        /// State Description
        /// </summary>
        public string StateDescription { get; set; }

        #region Navigation Properties

        /// <summary>
        /// Projects Collection
        /// </summary>
        public List<Project> Projects { get; set; }

        #endregion // Navigation Properties

    } // end public class ProjectStateDescription

} // end namespace QuantumWeb.Model

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:

C#
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);
        } // end public void Configure(EntityTypeBuilder<Project> builder)

    } // end public class ProjectConfiguration : IEntityTypeConfiguration<Project>

} // end namespace QuantumWeb.Data

Take a look at the extracted lines below:

C#
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:

C#
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.

C#
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:

C#
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using QuantumWeb.Model;

namespace QuantumWeb.Data
{
    /// <summary>
    /// ProjectState Description Configuration Class
    /// </summary>
    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);
        } // end public void Configure(EntityTypeBuilder<ProjectStateDescription> builder)

    } // end public class ProjectStateDescriptionConfiguration : 
      // IEntityTypeConfiguration<ProjectStateDescription>

} // end namespace QuantumWeb.Data

Now, we update the QuantumDbContext class.

Second update to QuantumDbContext.cs:

C#
using Microsoft.EntityFrameworkCore;
using QuantumWeb.Model;

namespace QuantumWeb.Data
{
    public class QuantumDbContext : DbContext
    {
        public QuantumDbContext (DbContextOptions<QuantumDbContext> options)
            : base(options)
        {
        } // end public QuantumDbContext (DbContextOptions<QuantumDbContext> options)

        #region DbSets

        /// <summary>
        /// Customer DbSet
        /// </summary>
        public DbSet<Customer> Customers { get; set; }

        /// <summary>
        /// Project DbSet
        /// </summary>
        public DbSet<Project> Projects { get; set; }

        /// <summary>
        /// ProjectStateDescription DbSet
        /// </summary>
        public DbSet<ProjectStateDescription> ProjectStateDescriptions { get; set; }

        #endregion // DbSets

        /// <summary>
        /// Data Model Creation Method
        /// </summary>
        /// <param name="modelBuilder">ModelBuilder instance</param>
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new CustomerConfiguration());
            modelBuilder.ApplyConfiguration(new ProjectConfiguration());
            modelBuilder.ApplyConfiguration(new ProjectStateDescriptionConfiguration());
        } // end  protected override void OnModelCreating(ModelBuilder modelBuilder)

    } // end public class QuantumDbContext : DbContext

} // end namespace QuantumWeb.Data

The added lines are shown in bold. Now add an EF migration for the Project and ProjectState entities.

C#
Add-Migration Added-Project-ProjectState

Generated ~\Migrations\20181021203503_Added-Project-ProjectState.cs:

C#
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:

Image 1

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:

HTML
@page
@model QuantumWeb.Pages.Customers.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
    <!-- A link to the Pages/Customers/Create page to create a new Customer -->
</p>
<!-- An HTML table to display existing Customers -->
<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 link to the Pages/Customers/Edit page to edit an existing Customer -->
                <a asp-page="./Details" asp-route-id="@item.CustomerId">Details</a> |
                <!--
                   A link to the Pages/Customers/Details page to display the details for an existing
                   Customer
                -->
                <a asp-page="./CustomerProjects" asp-route-id="@item.CustomerId">Projects</a> |
                <!--
                    A link to the Pages/Customers/CustomerProjects page to display & manage the
                    Projects for an existing Customer
                -->
                <a asp-page="./Delete" asp-route-id="@item.CustomerId">Delete</a>
                <!-- A link to the Pages/Customers/Delete page to delete an existing Customer -->
            </td>
        </tr>
}
    </tbody>
</table>

We will scaffold several custom Customers Razor Pages as follows.

Custom Scaffold for Customers Razor Pages:

Image 2

Scaffold Customers/CustomerProjects Razor Page:

Image 3

Clicking "Add" will produce shell files for the CustomerProjects Index page.

Generated ~Pages\Customers\CustomerProjects.cshtml
HTML
@page
@model QuantumWeb.Pages.Customers.CustomerProjectsModel
@{
    ViewData["Title"] = "CustomerProjects";
}

<h2>CustomerProjects</h2>
Generated ~Pages\Customers\CustomerProjects.cshtml.cs
C#
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
HTML
@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
C#
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;
        } // end public CustomerProjectsModel(QuantumDbContext context)

        public Customer Customer { get; set; }

        public async Task<IActionResult> OnGet(int? id)
        {
            if (id == null)
            {
                return NotFound();
            } // endif (id == null)

            Customer = await _context.Customers
                .Include(c => c.Projects)
                    .FirstOrDefaultAsync(c => c.CustomerId == id);

            if (Customer == null)
            {
                return NotFound();
            } // endif (Customer == null)

            return Page();
        } // end public async Task<IActionResult> OnGet(int? id)

    } // end public class CustomerProjectsModel : PageModel

} // end namespace QuantumWeb.Pages.Customers

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.

Image 4

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

Image 5

The "Create New Project" link will activate the custom CustomerProjectCreate Razor page. We now scaffold this page.

Scaffold Customers/CustomerProjectCreate Razor Page:

Image 6

Initial ~Pages\Customers\CustomerProjectCreate.cshtml.cs
C#
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;
        } // end public CustomerProjectCreateModel(QuantumContext context)

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnGet(int? id)
        {
            if (id == null)
            {
                return NotFound();
            } // endif (id == null)

            Customer = await _context.Customers
                .Include(c => c.Projects)
                    .FirstOrDefaultAsync(c => c.CustomerId == id);

            if (Customer == null)
            {
                return NotFound();
            } // endif (Customer == null)

            ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions,
                "ProjectStateCode", "StateDescription", ProjectState.Prospect);

            return Page();

        } // end public async Task<IActionResult> OnGet(int? id)

        [BindProperty]
        public Project Project { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            } // endif (!ModelState.IsValid)

            Project.CustomerId = Customer.CustomerId;

            _context.Projects.Add(Project);
            await _context.SaveChangesAsync();

            return RedirectToPage("./CustomerProjects", new { id = Customer.CustomerId });
        } // end public async Task<IActionResult> OnPostAsync()

    } // end public class CustomerProjectCreateModel : PageModel

} // end namespace QuantumWeb.Pages.Customers

Please take note of these lines in this code.

C#
[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.

C#
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.

C#
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:

HTML
@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:

HTML
<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.
ASP.NET
<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:

Image 7

This shows the Customers/CustomerProjectCreate page as initially displayed.

CustomerProjectCreate Page with data:

Image 8

After clicking "Create", we will see:

Customer Projects Page with added Project:

Image 9

The next two figures show things after other Projects are added for both Customers.

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

Image 10

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

Image 11

We can now add another page to edit the Customer Projects, the CustomerProjectEdit page.

Scaffold Customers/CustomerProjectEdit Razor Page

Image 12

Initial ~Pages\Customers\CustomerProjectEdit.cshtml.cs
C#
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;
        } // end public CustomerProjectEditModel(QuantumDbContext 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();
            } // endif (id == null)

            Project = await _context.Projects
                .Include(p => p.Customer)
                    .FirstOrDefaultAsync(p => p.ProjectId == id);

            if (Project == null)
            {
                return NotFound();
            } // endif (Project == null)

            Customer = Project.Customer;

            ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions,
                "ProjectStateCode", "StateDescription", ProjectState.Prospect);

            return Page();
        } // end public async Task<IActionResult> OnGet(int? id)

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            } // endif (!ModelState.IsValid)

            var projectToUpdate = await _context.Projects.FindAsync(id);

            if (projectToUpdate == null)
            {
                return NotFound();
            } // endif (projectToUpdate == null)

            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();
        } // end public async Task<IActionResult> OnPostAsync(int? id)

    } // end public class CustomerProjectEditModel : PageModel

} // end namespace QuantumApp.Pages.Customers

This code has the same artifacts as the CustomerProjectCreate page with regard to the .Include and the ViewData.

Initial ~Pages\Customers\CustomerProjectEdit.cshtml
HTML
@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:

Image 13

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

Image 14

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

Image 15

The final feature for this part Project deletion via the CustomerProjectDelete page.

Scaffold Customers/CustomerProjectDelete Razor Page:

Image 16

Initial ~Pages\Customers\CustomerProjectDelete.cshtml.cs
C#
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;
        } // end public CustomerProjectDeleteModel(QuantumContext 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();
            } // endif (id == null)

            Project = await _context.Projects
                .Include(p => p.Customer)
                    .FirstOrDefaultAsync(p => p.ProjectId == id);

            if (Project == null)
            {
                return NotFound();
            } // endif (Project == null)

            Customer = Project.Customer;

            return Page();
        } // end public async Task<IActionResult> OnGet(int? id)

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            } // endif (id == null)

            Project = await _context.Projects
                .Include(p => p.Customer)
                .FirstOrDefaultAsync(p => p.ProjectId == id);

            if (Project != null)
            {
                _context.Projects.Remove(Project);
                await _context.SaveChangesAsync();
            } // endif (Project != null)

            return RedirectToPage("./CustomerProjects", new { id = Project.Customer.CustomerId });
        } // end public async Task<IActionResult> OnPostAsync(int? id)

    } // end public class CustomerProjectDeleteModel : PageModel

} // end namespace QuantumWeb.Pages.Customer
Initial ~Pages\Customers\CustomerProjectDelete.cshtml
HTML
@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:

Image 17

Delete Customer Projects Page - Delete Ouachita Shale:

Image 18

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

Image 19

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)