Hi So I am using ASP.NET Core 2.2 with EF Core with SQL Server DB. I got a database that looks like this. So I have 3 tables in SQL Server. tblJobs, tblResults and tblProblems and others I omitted as they are not part of the problem.
Entity Relationship Diagram and gif demo of problem
I am attempting to read related data across these three tables. I am using Eager Loading to read the model field "Job.JobTitle" in the "ProblemsController.Index" method with this tutorial. https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/read-related-data?view=aspnetcore-2.2
I decided to manually insert in a query window into Result table the JobID matching the JobTitle I wanted with the ProblemID. It has now rendered the correct JobTitle in the Problem Index view record which is the value of "Pitcher". But that's only because I manually inserted it in tblResult which doesn't really help the end user. I'm wondering what's a way of them getting past that manual insertion.
Here is the my TeamContext class.
using Pitcher.Models; using Microsoft.EntityFrameworkCore; using Pitcher.Models.TeamViewModels; namespace Pitcher.Data { public class TeamContext : DbContext { public TeamContext(DbContextOptions<TeamContext> options) : base(options) { } public DbSet<User> Users { get; set; } public DbSet<Registration> Registrations {get;set;} public DbSet<Job> Jobs {get;set;} public DbSet<Problem> Problems { get; set; } public DbSet<Result> Results {get;set;} protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>().ToTable("tblUser"); modelBuilder.Entity<Registration>().ToTable("tblRegistration"); modelBuilder.Entity<Job>().ToTable("tblJob"); modelBuilder.Entity<Problem>().ToTable("tblProblem"); modelBuilder.Entity<Chat>().ToTable("tblChat"); modelBuilder.Entity<Result>().ToTable("tblResult"); modelBuilder.Entity<Result>() .HasKey(bc => new { bc.JobID, bc.ProblemID }); modelBuilder.Entity<Result>() .HasOne(bc => bc.Job) .WithMany(b => b.Results) .HasForeignKey(bc => bc.JobID); modelBuilder.Entity<Result>() .HasOne(bc => bc.Problem) .WithMany(c => c.Result) .HasForeignKey(bc => bc.ProblemID); } } }
Here are the 3 models.
Job model:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace Pitcher.Models { public class Job { public int ID { get; set; } [Required] [StringLength(20, MinimumLength = 3, ErrorMessage = "Job Title must be bettween 3 to 20 characters.")] [DataType(DataType.Text)] [Display(Name = "Job Title")] [Column("JobTitle")] public string JobTitle { get; set; } [StringLength(200, MinimumLength = 3, ErrorMessage = "Job Description must be bettween 200 to 3 characters.")] [DataType(DataType.Text)] [Display(Name = "Description")] [Column("JobDescription")] public string JobDescription { get; set; } [Required] [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [Display(Name = " Start Date")] [Column("JobStartDate")] public DateTime JobStartDate {get;set;} [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [Display(Name = "Deadline Date")] [Column("JobDeadlineDate")] public DateTime JobDeadline {get;set;} [Display(Name = "Job Is Complete?")] [Column("JobIsComplete")] public bool JobIsComplete{get;set;} public ICollection<Registration> Registrations {get;set;} public ICollection<Result> Results {get;set;} } }
Result model:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Pitcher.Models { public class Result { public int JobID {get;set;} public int ProblemID {get;set;} public Job Job {get;set;} public Problem Problem {get;set;} } }
Problem model:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace Pitcher.Models { public class Problem { public int ID {get;set;} [Required] [StringLength(180, MinimumLength = 2, ErrorMessage = "Problem Title must be bettween 2 to 20 characters.")] [DataType(DataType.Text)] [Display(Name = "Problem Title")] [Column("ProblemTitle")] public string ProblemTitle {get;set;} [Required] [StringLength(int.MaxValue, MinimumLength = 5, ErrorMessage = "Problem Title must be at least 5 characters.")] [DataType(DataType.Text)] [Display(Name = "Problem Description")] [Column("ProblemDescription")] public string ProblemDescription {get;set;} [Required] [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [Display(Name = " Problem Start Date")] [Column("ProblemStartDate")] public DateTime ProblemStartDate {get;set;} [DataType(DataType.Upload)] [Display(Name = " Upload file")] [Column("ProblemFileAttachments")] public string ProblemFileAttachments {get;set;} [Required] [Display(Name = "Problem Severity")] [Range(1,5, ErrorMessage = "Problem Severity value for {0} must be between {1} and {2}.")] [Column("ProblemSeverity")] public int ProblemSeverity {get;set;} [Display(Name = "Problem Complete")] [Column("ProblemComplete")] public bool ProblemComplete {get;set;} public ICollection<Result> Result {get;set;} public ICollection<Chat> Chat {get;set;} } }
Here are the 2 Controllers I'm using with their Index methods only.
Job Index controller method:
public async Task<IActionResult> Index(string sortOrder, string currentFilter, string searchString, int? pageNumber) { ViewData["CurrentSort"] = sortOrder; ViewData["JobTitleSortParm"] = sortOrder == "JobStartDate" ? "JobTitle_desc" : "JobStartDate"; ViewData["JobStartDateSortParm"] = sortOrder == "JobStartDate" ? "JobStart_date_desc" : "JobStartDate"; ViewData["JobDeadlineDateSortParm"] = sortOrder == "JobDeadlineDate" ? "JobDeadline_date_desc" : "JobDeadlineDate"; ViewData["CurrentFilter"] = searchString; var jobs = from j in _context.Jobs select j; if (searchString != null) { pageNumber = 1; } else { searchString = currentFilter; } if (!String.IsNullOrEmpty(searchString)) { jobs = jobs.Where(j => j.JobTitle.Contains(searchString) || j.JobDescription.Contains(searchString)); } switch (sortOrder) { case "JobTitle_desc": jobs = jobs.OrderByDescending(j => j.JobTitle); break; case "JobStartDate": jobs = jobs.OrderBy(j => j.JobStartDate); break; case "JobStart_date_desc": jobs = jobs.OrderByDescending(j => j.JobStartDate); break; case "JobDeadline_date_desc": jobs = jobs.OrderByDescending(j => j.JobDeadline); break; case "JobDeadlineDate": jobs = jobs.OrderBy(j => j.JobDeadline); break; //By default JobTitle is in ascending order when entity is loaded. default: jobs = jobs.OrderBy(j => j.JobTitle); break; } int pageSize = 20; return View(await PaginatedList<Job>.CreateAsync(jobs.AsNoTracking(), pageNumber ?? 1, pageSize)); }
Problem Index controller method:
public async Task<IActionResult> Index(string sortOrder, string currentFilter, string searchString, int? pageNumber) { ViewData["CurrentSort"] = sortOrder; ViewData["ProblemIDSortParm"] = sortOrder == "ProblemID" ? "ProblemID_desc" : "ProblemID"; ViewData["ProblemTitleSortParm"] = sortOrder == "ProblemTitle" ? "ProblemTitle_desc" : "ProblemTitle"; ViewData["ProblemStartDateSortParm"] = sortOrder == "ProblemStartDate" ? "ProblemStartDate_desc" : "ProblemStartDate"; ViewData["ProblemSeveritySortParm"] = sortOrder == "ProblemSeverity" ? "ProblemSeverity_desc" : "ProblemSeverity"; ViewData["ProblemCompleteSortParm"] = sortOrder == "ProblemComplete" ? "ProblemComplete_desc" : "ProblemComplete"; ViewData["CurrentFilter"] = searchString; //READ RELATED DATA HERE var problems = from p in _context.Problems .Include(p => p.Result) .ThenInclude(j => j.Job) select p; //END OF READ RELATED DATA if(searchString != null) { pageNumber = 1; } else { searchString = currentFilter; } if(!String.IsNullOrEmpty(searchString)) { problems = problems.Where(p => p.ProblemTitle.Contains(searchString) || p.ProblemDescription.Contains(searchString)); } switch (sortOrder) { case "ProblemID_desc": problems = problems.OrderByDescending(p => p.ID); break; case "ProblemTitle_desc": problems = problems.OrderByDescending(p => p.ProblemTitle); break; case "ProblemTitle": problems = problems.OrderBy(p => p.ProblemTitle); break; case "ProblemStartDate": problems = problems.OrderBy(p => p.ProblemStartDate); break; case "ProblemStartDate_desc": problems = problems.OrderByDescending(p => p.ProblemStartDate); break; case "ProblemSeverity": problems = problems.OrderBy(p => p.ProblemSeverity); break; case "ProblemSeverity_desc": problems = problems.OrderByDescending(p => p.ProblemSeverity); break; case "ProblemComplete": problems = problems.OrderBy(p => p.ProblemComplete); break; case "ProblemComplete_desc": problems = problems.OrderByDescending(p => p.ProblemComplete); break; default: problems = problems.OrderBy(p => p.ID); break; } int pageSize = 20; return View(await PaginatedList<Problem>.CreateAsync(problems.AsNoTracking(), pageNumber ?? 1, pageSize)); }
And the Index view for Problem:
@model PaginatedList<Pitcher.Models.Problem> @{ ViewData["Title"] = "Problems"; }<h1>Problems</h1><p><a asp-action="Create">Create New</a></p> @*COPY AND PASTE THIS TAG HELPER METHOD TEXTBOX CUSTOMIZATION INTO OTHER VIEWS TO ENABLE SEARCHING.*@<form asp-action="Index" method="get"><div class="form-actions no-color"><p> Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" /><input type="submit" value="Search" button type="button" class="btn btn-primary" /> |<a asp-action="Index">Back to Full List </a></p></div></form><table class="table table-hover"><thead><tr><th><a asp-action="Index" asp-route-sortOrder="@ViewData["ProblemIDSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Problem ID</a></th><th><a asp-action="Index" asp-route-sortOrder="@ViewData["ProblemTitleSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Problem Title</a></th><th> Description</th><th><a asp-action="Index" asp-route-sortOrder="@ViewData["ProblemStartDateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Problem Start Date</a></th><th> Problem File Attachments</th><th><a asp-action="Index" asp-route-sortOrder="@ViewData["ProblemSeveritySortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">ProblemSeverity</a></th><th><a asp-action="Index" asp-route-sortOrder="@ViewData["ProblemCompleteSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">ProblemComplete</a></th><th> Job Title</th><th></th></tr></thead><tbody> @foreach (var item in Model) {<tr><td> @Html.DisplayFor(modelItem => item.ID)</td><td> @Html.DisplayFor(modelItem => item.ProblemTitle)</td><td> @Html.DisplayFor(modelItem => item.ProblemDescription)</td><td> @Html.DisplayFor(modelItem => item.ProblemStartDate)</td><td> @Html.DisplayFor(modelItem => item.ProblemFileAttachments)</td><td> @Html.DisplayFor(modelItem => item.ProblemSeverity)</td><td> @Html.DisplayFor(modelItem => item.ProblemComplete)</td><td> @foreach (var title in item.Result) { @Html.DisplayFor(modelItem => title.Job.JobTitle)<br /> }</td><td><a asp-action="Edit" asp-route-id="@item.ID" button type="button" class="btn btn-primary btn-block" >Edit</a> <a asp-action="Details" asp-route-id="@item.ID" button type="button" class="btn btn-info btn-block">Details</a> <a asp-action="Delete" asp-route-id="@item.ID" button type="button" class="btn btn-primary btn-block">Delete</a></td></tr>}</tbody></table> @{ var prevDisabled = !Model.HasPreviousPage ? "disabled" : ""; var nextDisabled = !Model.HasNextPage ? "disabled" : ""; }<a asp-action="Index" asp-route-sortOrder="@ViewData["CurrentSort"]" asp-route-pageNumber="@(Model.PageIndex - 1)" asp-route-currentFilter="@ViewData["CurrentFilter"]" class="btn btn-secondary @prevDisabled" button type="button"> Previous</a><a asp-action="Index" asp-route-sortOrder="@ViewData["CurrentSort"]" asp-route-pageNumber="@(Model.PageIndex + 1)" asp-route-currentFilter="@ViewData["CurrentFilter"]" class="btn btn-secondary @nextDisabled" button type="button"> Next</a>
Kind regards,
Jordan Nash