Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

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

5.00/5 (7 votes)
23 Nov 2018CPOL7 min read 15.4K   191  
Add Project and Project Skills Processing

Introduction

This is Part IV of a multi-part article which 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 to bridge these two worlds and facilitate the transfer of data between them.

Part I. Setup the Entity Framework data context and the initial Customers Razor Pages

Part II. Complete CRUD functions for Customers

Part III. Create Project and ProjectState entities and implement a one-to-many relationship between ProjectState and Projects

In Part IV: Add Skill entities (Skill enum, SkillTitle and ProjectSkill) and implement a many-to-many relationship between Projects and Skills as follows:

  • Add entities Skill, SkillTitle and ProjectSkill and configure into QuantumDbContext.
  • Add migration, Add-Project-Skill-Entities, and Update-Database.
  • Scaffold Skill related Razor pages, CustomerProjectSkills.cshtml, CustomerProjectSkillAssign.cshtml and CustomerProjectSkillDelete.cshtml. Run tests to confirm functions.

Background

The sample application implemented in this series is for a fictitious engineering and technology company, Quantum Engineering Technologies. Quantum primarily serves the oil, gas and chemical industries. To this point, we have discussed the basis of the object model and created a number of ASP.NET Razor Pages to handle Customers, Projects and Project states. In this final part, we will discuss listing of the skills required to execute the work. This effort is built around a Skill enum and the many-to-many relationship between Skills and Projects. There can be many Projects requiring a certain Skill. There can, also, be many Skills needed to execute a Project. Hence, there exists a many-to-many relationship between the Project and Skill entities.

In relational databases, many-to-many relationships are implemented by creating a join table for the two entities as shown below.

Many-to-many Relationship via Join Table

Image 1

There is a many-to-many relationship between TABLE A and TABLE B. In a relational database, one creates a join table, JOIN-TABLE A-B, with a one-to-many relationship with both tables.

In previous versions of Entity Framework (EF), the join table could be configured by convention without the need for an entity or class in the object model. However, this feature has not been provided in Entity Framework Core as of this date. (See this link.) Therefore, we need to create and configure a class in our object model to achieve this end. The following discussion shows how this is done along with the associated Razor Pages to manage things in the UI.

Using the Code

The working object model is shown in Part I and repeated here for Skills.

Working Object Model for Skills

Image 2

Skill.cs

HTML
namespace QuantumWeb.Model
{
    /// <summary>
    /// Skill Enumeration
    /// </summary>
    public enum Skill
    {
        Programmer, // Programmer
        ME,         // Mechanical Engineer
        ChE,        // Chemical Engineer
        CtrlE,      // Control Engineer
        SWE,        // Software Engineer
        SWArch,     // Software Architect
        WebDes,     // Web Designer
        DataSci,    // Data Scientist
        ProjMgr,    // Project Manager
        BusAnal,    // Business Analyst
        QA          // Quality Assurance Tester
    } // end public enum Skill

} // end namespace QuantumWeb.Model

We provide a SkillTitle class so we can provide associated titles for enum values in the UI.

SkillTitle.cs

HTML
using System.Collections.Generic;

namespace QuantumWeb.Model
{
    /// <summary>
    /// SkillTitle Class
    /// </summary>
    public class SkillTitle
    {
        /// <summary>
        /// Skill Code
        /// </summary>
        public Skill SkillCode { get; set; }
        /// <summary>
        /// Skill Title
        /// </summary>
        public string Title { get; set; }

        #region Navigation Properties

        /// <summary>
        /// List of ProjectSkill Instances
        /// </summary>
        public List<ProjectSkill> ProjectSkills { get; set; }

        #endregion // Navigation Properties

    } // end public class SkillTitle

} // end namespace QuantumWeb.Model

This class references the class, ProjectSkill, which will be configured to the join table in the database.

ProjectSkill.cs

HTML
namespace QuantumWeb.Model
{
    /// <summary>
    /// ProjectSkill Class, a join entity
    /// </summary>
    /// <remarks>
    /// Note: The database table will have a non-unique index on ProjectId and SkillCode
    /// </remarks>
    public class ProjectSkill
    {

        /// <summary>
        /// ProjectSkill Identifier, primary key
        /// </summary>
        public int ProjectSkillId { get; set; }

        #region Navigation Properties

        /// <summary>
        /// Project Identifier
        /// </summary>
        public int ProjectId { get; set; }
        /// <summary>
        /// Project Reference
        /// </summary>
        public Project Project { get; set; }
        /// <summary>
        /// Skill Code
        /// </summary>
        public Skill SkillCode { get; set; }
        /// <summary>
        /// SkillTitle Reference
        /// </summary>
        public SkillTitle SkillTitle { get; set; }

        #endregion // Navigation Properties

    } // end public class ProjectSkill

} // end namespace QuantumWeb.Model

Notice the navigation properties. There is a reference to a single Project instance and a single SkillTitle instance. The ProjectId and SkillCode properties will map to foreign keys in the ProjectSkills table in the database. Also, notice that the SkillTitle class has a collection property, ProjectSkills (a List<ProjectSkill>), that indicates it can be related to zero or more ProjectSkill instances. We now must modify the Project class.

Modified Project.cs

HTML
using System.Collections.Generic;

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; }

        /// <summary>
        /// List of ProjectSkill Instances
        /// </summary>
        public List<ProjectSkill> ProjectSkills { get; set; }

        #endregion // Navigation Properties

    } // end public class Project

} // end namespace QuantumApp.Model

Here, we added a navigation property, ProjectSkills (a List<ProjectSkill>), which allows a Project to be related to multiple ProjectSkill instances.

Now we must configure these classes in the QuantumDbContext class to create the database mappings. First, we create the SkillTitleConfiguration class.

SkillTitleConfiguration.cs

HTML
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using QuantumWeb.Model;

namespace QuantumWeb.Data
{
    public class SkillTitleConfiguration : IEntityTypeConfiguration<SkillTitle>
    {
        public void Configure(EntityTypeBuilder<SkillTitle> builder)
        {
            builder.ToTable("SkillTitles");
            builder.HasKey(st => st.SkillCode);
            builder.Property(st => st.SkillCode)
            .HasColumnType("nvarchar(20)")
            .HasConversion(
                st => st.ToString(),
                st => (Skill)Enum.Parse(typeof(Skill), st));
            builder.Property(st => st.Title)
            .IsRequired()
            .HasColumnType("nvarchar(50)")
            .HasMaxLength(50);
        } // end public void Configure(EntityTypeBuilder<SkillTitle> builder)

    } // end public class SkillTitleConfiguration : IEntityTypeConfiguration<SkillTitle>

} // end namespace QuantumWeb.Data

This maps the SkillTitle class to the SkillTitles database table. It also configures the conversion between the Skill enum values and the SkillCode string column values in the SkillTitles table. This is similar to the mapping of the ProjectState enum values to the ProjectStateCode values discussed in Part III.

We can now configure the mapping of the ProjectSkill class to the join table, ProjectSkills.

ProjectSkillConfiguration.cs

HTML
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using QuantumWeb.Model;

namespace QuantumWeb.Data
{
    public class ProjectSkillConfiguration : IEntityTypeConfiguration<ProjectSkill>
    {
        public void Configure(EntityTypeBuilder<ProjectSkill> builder)
        {
            builder.ToTable("ProjectSkills");
            builder.HasKey(ps => ps.ProjectSkillId);
            builder.Property(ps => ps.ProjectSkillId)
            .HasColumnType("int");
            builder.Property(ps => ps.ProjectId)
            .HasColumnType("int");
            builder.Property(ps => ps.SkillCode)
            .HasColumnType("nvarchar(20)")
            .HasConversion(
                ps => ps.ToString(),
                ps => (Skill)Enum.Parse(typeof(Skill), ps));
            builder.HasIndex(ps => new { ps.ProjectId, ps.SkillCode })
            .IsUnique(false);
            builder.HasOne<Project>(ps => ps.Project)
                .WithMany(p => p.ProjectSkills)
                .HasForeignKey(ps => ps.ProjectId);
            builder.HasOne<SkillTitle>(ps => ps.SkillTitle)
                .WithMany(p => p.ProjectSkills)
                .HasForeignKey(ps => ps.SkillCode);
        } // end public void Configure(EntityTypeBuilder<ProjectSkill> builder)

    } // end public class ProjectSkillConfiguration : IEntityTypeConfiguration<ProjectSkill>

} // end namespace QuantumWeb.Data

We now modify the QuantumDbContext class to reflect these changes.

Modified 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; }

        /// <summary>
        /// SkillTitle DbSet
        /// </summary>
        public DbSet<SkillTitle> SkillTitles { get; set; }

        /// <summary>
        /// ProjectSkill DbSet
        /// </summary>
        public DbSet<ProjectSkill> ProjectSkills { 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());
            modelBuilder.ApplyConfiguration(new SkillTitleConfiguration());
            modelBuilder.ApplyConfiguration(new ProjectSkillConfiguration());
        } // end  protected override void OnModelCreating(ModelBuilder modelBuilder)

    } // end public class QuantumDbContext : DbContext

} // end namespace QuantumWeb.Data

Here we define the DbSets, SkillTitles and ProjectSkills, and add the SkillTitleConfiguration and ProjectSkillConfiguration classes for processing in the OnModelCreating method. Now we create a migration and update the database in the Package Manager Console.

Add-Migration Add-Project-Skill-Entities

Generated 20181025222456_Add-Project-Skill-Entities.cs

HTML
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;

namespace QuantumWeb.Migrations
{
    public partial class AddProjectSkillEntities : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "SkillTitles",
                columns: table => new
                {
                    SkillCode = table.Column<string>(type: "nvarchar(20)", nullable: false),
                    Title = table.Column<string>(type: "nvarchar(50)", 
                            maxLength: 50, nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_SkillTitles", x => x.SkillCode);
                });

            migrationBuilder.CreateTable(
                name: "ProjectSkills",
                columns: table => new
                {
                    ProjectSkillId = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy",
                            SqlServerValueGenerationStrategy.IdentityColumn),
                    ProjectId = table.Column<int>(type: "int", nullable: false),
                    SkillCode = table.Column<string>(type: "nvarchar(20)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_ProjectSkills", x => x.ProjectSkillId);
                    table.ForeignKey(
                        name: "FK_ProjectSkills_Projects_ProjectId",
                        column: x => x.ProjectId,
                        principalTable: "Projects",
                        principalColumn: "ProjectId",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_ProjectSkills_SkillTitles_SkillCode",
                        column: x => x.SkillCode,
                        principalTable: "SkillTitles",
                        principalColumn: "SkillCode",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_ProjectSkills_SkillCode",
                table: "ProjectSkills",
                column: "SkillCode");

            migrationBuilder.CreateIndex(
                name: "IX_ProjectSkills_ProjectId_SkillCode",
                table: "ProjectSkills",
                columns: new[] { "ProjectId", "SkillCode" });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "ProjectSkills");

            migrationBuilder.DropTable(
                name: "SkillTitles");
        }
    }
}

There are some key points to consider.

  1. The SkillTitles table has a primary key, SkillCode, which maps to a value in the Skill enum.
  2. The ProjectSkills table has an integer primary key, ProjectSkillId, and two foreign keys, ProjectId, to link to the Project record and, SkillCode, to link to the SkillTitle record. Some writers recommend setting a multi-valued primary key, ProjectId and SkillCode. That would setup some issues with a Project needing multiple skills of the same type. We do set up a non-unique index, IX_ProjectSkills_ProjectId_SkillCode, however.

We will see some of these features in action later. Now to move the migration to the database.

Update-Database

The diagram below generated from SQL Server Management Studio (SSMS) shows the relevant database tables.

Customers-Projects-Skills Database Diagram

Image 3

Now we include these entities in the UI. First, we create a link to ProjectSkills in the CustomerProjects Index page.

Modified CustomerProject.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="./CustomerProjectSkills" 
                        asp-route-id="@item.ProjectId">
                            ProjectSkills
                        </a> |
                        <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 link to CustomerProjectSkills requires that we scaffold a new Razor Page.

Scaffold Customers/CustomerProjectSkills Razor Page

Image 4

Initial ~Pages\Customers\CustomerProjectSkills.cshtml

HTML
@page "{id:int?}"
@model QuantumWeb.Pages.Customers.CustomerProjectSkillsModel
@{
    ViewData["Title"] = "Customer Project Skills";
}

<h2>Customer Project Skills</h2>

<p>
    <a asp-page="./CustomerProjects" 
     asp-route-id="@Model.Customer.CustomerId">Back to List</a> |
    <a asp-page="CustomerProjectSkillAssign" 
     asp-route-id="@Model.Project.ProjectId">Assign Skill</a>
</p>
<div>
    <h4>Customer-Project</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.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.ProjectSkills)
        </dt>
        <dd>
            <table class="table">
                <tr>
                    <th>Project Skill Id</th>
                    <th>Skill Code</th>
                    <th>Skill Title</th>
                    <th></th>
                </tr>
                @foreach (var item in Model.Project.ProjectSkills)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.ProjectSkillId)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.SkillCode)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.SkillTitle.Title)
                        </td>
                        <td>
                            <a asp-page="./CustomerProjectSkillDelete"
                              asp-route-id="@item.ProjectSkillId">
                                  Delete
                            </a>
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>

This configures the page to use a nullable parameter, id, which will allow the ProjectId of the target Project to be passed to the OnGet handler. There is a link to a CustomerProjectSkillAssign page which will create a ProjectSkill record effectively assigning a Skill to the Project. There is another link to the CustomerProjectSkillDelete page which will delete a ProjectSkill record. We will show code for these two pages later.

Initial ~Pages\Customers\CustomerProjectSkills.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 CustomerProjectSkillsModel : PageModel
    {

        private readonly QuantumDbContext _context;

        public CustomerProjectSkillsModel(QuantumDbContext context)
        {
            _context = context;
        } // end public CustomerProjectSkillsModel(QuantumDbContext context)

        public Project Project { get; set; }
        public Customer Customer { 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)
                .Include(p => p.ProjectSkills)
                    .ThenInclude(ps => ps.SkillTitle)
                    .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)

    } // end public class CustomerProjectSkillsModel : PageModel

} // end namespace QuantumWeb.Pages.Customers

Please take note of the example of Entity Framework(EF) eager loading.

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

In EF, eager loading related entities are retrieved in a single query using the Include() and ThenInclude extension methods. Here, we retrieve the related Customer and ProjectSkills entities for the Project whose ProjectId has the value of the input parameter, id. The query goes one step further using the ThenInclude() method to retrieve the SkillTitles associated with the retrieved ProjectSkills.

The next few screenshots show the sequence to navigate to this page.

QuantumWeb Application Home Page: https//localhost: 44306/

Image 5

QuantumWeb Application Customer Index Page: https//localhost: 44306/Customers

Image 6

Customer Projects Page with 2 Projects for Mirarex Oil & Gas (ProjectSkills link added)

Image 7

Customer Project Skills Page for Mirarex Oil & Gas, Zolar Pipeline (No Skills)

Image 8

We now show the scaffolding of the CustomerProjectSkillAssign Razor Page.

Scaffold Customers/CustomerProjectSkillAssign Razor Page

Image 9

Initial ~Pages\Customers\CustomerProjectSkillAssign.cshtml.cs

HTML
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 CustomerProjectSkillAssignModel : PageModel
    {
        private readonly QuantumDbContext _context;

        public CustomerProjectSkillAssignModel(QuantumDbContext context)
        {
            _context = context;
        } // end public CustomerProjectSkillAssignModel(QuantumDbContext context)

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

        [BindProperty]
        public SkillTitle SkillTitle { 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)
                .Include(p => p.ProjectSkills)
                    .ThenInclude(ps => ps.SkillTitle)
                    .FirstOrDefaultAsync(p => p.ProjectId == id);

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

            Customer = Project.Customer;

            ViewData["SkillCode"] = new SelectList(_context.SkillTitles, "SkillCode", "Title");

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

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

            ProjectSkill projectSkill = new ProjectSkill()
            {
                ProjectId = Project.ProjectId,
                SkillCode = SkillTitle.SkillCode
            };

            _context.ProjectSkills.Add(projectSkill);
            await _context.SaveChangesAsync();

            return RedirectToPage("./CustomerProjectSkills", new { id = Project.ProjectId });
        } // end public async Task<IActionResult> OnPostAsync()

    } // end public class CustomerProjectSkillAssignModel : PageModel

} // end namespace QuantumWeb.Pages.Customers

Here again, we have a ViewData to setup a select list as in Part III.

C#
ViewData["SkillCode"] = new SelectList(_context.SkillTitles, "SkillCode", "Title");

This time, it handles the SkillTitles for selecting Skills to associate with a Project.

Initial ~Pages\Customers\CustomerProjectSkillAssign.cshtml

HTML
@page "{id:int?}"
@model QuantumWeb.Pages.Customers.CustomerProjectSkillAssignModel
@{
    ViewData["Title"] = "Customer Project Skill Assignment";
}

<h2>Customer Project Skill Assignment</h2>
<hr />
<dl class="dl-horizontal">
    <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.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="Project.ProjectId" />
            <div class="form-group">
                <label asp-for="SkillTitle.SkillCode" 
                class="control-label"></label>
                <select asp-for="SkillTitle.SkillCode" 
                class="form-control" asp-items="ViewBag.SkillCode"></select>
            </div>
            <div class="form-group">
                <a asp-page="./CustomerProjectSkills" 
                asp-route-id="Project.ProjectId">Project Skills</a> |
                <input type="submit" value="Assign" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

Customer Project Skill Assignment Page for Zolar Pipeline (Skill select open)

Image 10

Customer Project Skill Assignment Page for Zolar Pipeline (Assign a programmer)

Image 11

Customer Project Skills Page for Mirarex Oil & Gas, Zolar Pipeline (Programmer assigned)

Image 12

After assigning several more skills, this page looks like:

Customer Project Skills Page for Mirarex Oil & Gas, Zolar Pipeline (Several Skills)

Image 13

Management decides there are too many programmers assigned, so we need to delete one. We now will scaffold the CustomerProjectSkillDelete page.

Scaffold Customers/CustomerProjectSkillDelete Razor Page

Image 14

Initial ~Pages\Customers\CustomerProjectSkillDelete.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 CustomerProjectSkillDeleteModel : PageModel
    {
        private readonly QuantumDbContext _context;

        public CustomerProjectSkillDeleteModel(QuantumDbContext context)
        {
            _context = context;
        } // end public CustomerProjectSkillDeleteModel(QuantumDbContext 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("./CustomerProjectSkills", 
                                    new { id = Project.Customer.CustomerId });
        } // end public async Task<IActionResult> OnPostAsync(int? id)

    } // end public class CustomerProjectSkillDeleteModel : PageModel

} // end namespace QuantumWeb.Pages.Customers

Initial ~Pages\Customers\CustomerProjectSkillDelete.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 Project Skill Delete Page for Mirarex Oil & Gas, Zolar Pipeline

Image 15

Customer Project Skills Page for Mirarex Oil & Gas, Zolar Pipeline (Record deleted)

Image 16

Summary

We have implemented the Skill to Project many-to-many relationship and the join entity, ProjectSkill, to implement the feature in Entity Framework Core. We also created ASP.NET Razor Pages to manage the entities.

License

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