“Xaero” is a an Entity Framework Core project built in ASP.NET Core. It powers a Movie Database application. Entity Framework core also known as EF Core is the modern and popular Object/Relational Mapper (O/RM) framework by Microsoft. In this tutorial we will build a Very Advanced Project from scratch. So make sure you read this tutorial from start till end, the source codes are given to download. Link is given at the end of the tutorial.
We will use the following frameworks/programming languages to build this project:
You can download this project from my GitHub Repository.
Page Contents
This project will power a Movie Database application. It will communicate with the database through Entity Framework Core O/RM. The database will have 3 main tables for keeping information about Movies, Distributions & Production Companies, plus there will be the relationships among them. The Database Diagram is shown in the below image:
The relationships between the database tables are:
The Movie & ProductionCompany database tables will have One-to-Many Relationship. This means One Production Company can have Many Movies or many movies can have the same production company.
The Movie & MovieDetail database tables will have One-to-One Relationship. This means One Movie will have exactly one record in “MovieDetail” table and vice versa.
The Movie & Distribution database tables will have Many-to-Many Relationship. This means One Movie can be associated with multiple Distributions and similarly one Distribution can be associated with multiple movies.
To create this Many-to-Many Relationship between these 2 table, we have used a separate database table called MovieDistribution. You can refer it in the database diagram image.
The CRUD Operations on the Movie Database will be performed with EF Core.
This project will help ASP.NET Core programmers with little or absolutely no Entity Framework Core knowledge. They will learn to work with EF Core and create excellent database driven applications.
Programmers will learn how relationships like One-to-Many, One-to-One & Many-to-Many are integrated to database tables and how CRUD operations are performed on them. The scenarios covered are:
So let us start building this project from scratch.
Create a new ASP.NET Core MVC project and name is as Xaero. We have taken DOT NET version 5.0 for this project. Make sure you have 4 folders on the root of the App, which are:
Next go to the Startup.cs class and change the default code to as shown in the below highlighted code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Xaero
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Finally perform the Installation of Entity Framework Core in the project.
First we will need to create 5 Entity Classes inside the Models folder of the project. These entity classes are:
The ProductionCompany.cs class is the entity for Production Company. It will manage the details of the production companies stored in the database.
The code of the ProductionCompany.cs class is given below. Note that the DataAnnotations Attributes applied to it’s properties are meant for doing validations on the View and not for setting the data types for the database tables. We will instead use Fluent API for configuring the Database Schema.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace Xaero.Models
{
public class ProductionCompany
{
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Name { get; set; }
public string Logo { get; set; }
[Required]
[DisplayFormat(DataFormatString = "{0:n2}", ApplyFormatInEditMode = true)]
public decimal AnnualRevenue { get; set; }
[Required]
public DateTime EstablishmentDate { get; set; }
public ICollection<Movie> Movie_R { get; set; } // one-to-Many with Movie
}
}
The property called Movie_R is a Collection Navigation Property of Movie type which will be used to create One-to-Many Relationship between the ProductionCompany & Movie tables of the database. We have covered this topic in another tutorial called Conventions in Entity Framework Core.
The Movie.cs class will be the entity for the Movie object. It is given below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Xaero.Models
{
public class Movie
{
public int Id { get; set; }
public int ProductionCompanyId { get; set; }
public ProductionCompany ProductionCompany_R { get; set; } // one-to-Many with ProductionCompany
public MovieDetail MovieDetail_R { get; set; } // one-to-one with MovieDetail
public IList<MovieDistribution> MovieDistribution_R { get; set; } // Many-to-Many with Distribution
}
}
The ProductionCompanyId will be the foreign key for this table (i.e. the parent value will be the Id column of the ProductionCompany table).
It has 2 Reference Navigation Properties which are – ProductionCompany_R & MovieDetail_R. It also has a Collection Navigation Property called MovieDistribution_R.
The ProductionCompany_R is of type ProductionCompany and used for creating One-to-Many relationship with the ProductionCompany entity class.
The MovieDetail_R is of type MovieDetail and used for creating One-to-One relationship with the MovieDetail entity class.
Finally there is MovieDistribution_R is of type IList<MovieDistribution> and used for creating Many-to-Many relationship with the Distribution entity class. For doing this it uses another entity called MovieDistribution. We will explain this later on.
The MovieDetails.cs class contains the full information about the Movies. It is shown below.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace Xaero.Models
{
public class MovieDetail
{
public int MovieId { get; set; }
[Required]
[StringLength(50)]
public string Name { get; set; }
public string Poster { get; set; }
[Required]
[DisplayFormat(DataFormatString = "{0:n2}", ApplyFormatInEditMode = true)]
public decimal Budget { get; set; }
[Required]
[DisplayFormat(DataFormatString = "{0:n2}", ApplyFormatInEditMode = true)]
public decimal Gross { get; set; }
[Required]
public DateTime ReleaseDate { get; set; }
public Movie Movie_R { get; set; } // one-to-one with Movie
}
}
The MovieId column will be the foreign key for this table (i.e. the parent value will be the Id column of the Movie table).
The Movie_R of type Movie is creating One-to-One Relationship between this entity and the Movie.cs.
The Distribution.cs class will be the entity for the Distribution. It is given below:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace Xaero.Models
{
public class Distribution
{
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Name { get; set; }
[Required]
public string Location { get; set; }
[Required]
public string Telephone { get; set; }
public IList<MovieDistribution> MovieDistribution_R { get; set; } // Many-to-Many with Movie
}
}
The field called MovieDistribution_R is of type IList<MovieDistribution> and is used to create Many-to-Many Relationship between Distribution & Movie entities.
Note that another entity called MovieDistribution is used to create this Many-to-Many Relationship.
This is a Joining Entity whose sole purpose is to create Many-to-Many Relationship between the 2 entities which are:
The code of MovieDistribution.cs is given below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Xaero.Models
{
public class MovieDistribution
{
public int MovieId { get; set; } //foreign key property
public int DistributionId { get; set; } //foreign key property
public Movie Movie_R { get; set; } //Reference navigation property
public Distribution Distribution_R { get; set; } //Reference navigation property
}
}
The MovieId is the foreign key associated with the Movie entity while DistributionId is the foreign key associated with the Distribution entity.
The ’Movie_R’ & ‘Distribution_R’ fields are the Reference navigation property for creating Many-to-Many Relationship.
Create the Database Context Class called MovieContext.cs inside the Models folder. It’s code is given below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Xaero.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options) : base(options)
{
}
public DbSet<ProductionCompany> ProductionCompany { get; set; }
public DbSet<Movie> Movie { get; set; }
public DbSet<MovieDetail> MovieDetail { get; set; }
public DbSet<Distribution> Distribution { get; set; }
public DbSet<MovieDistribution> MovieDistribution { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ProductionCompany
modelBuilder.Entity<ProductionCompany>().HasKey(s => s.Id);
modelBuilder.Entity<ProductionCompany>(entity =>
{
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50);
entity.Property(e => e.Logo)
.IsRequired();
entity.Property(e => e.AnnualRevenue)
.IsRequired()
.HasColumnType("Money");
entity.Property(e => e.EstablishmentDate)
.IsRequired()
.HasColumnType("Date") ;
});
// Movie
modelBuilder.Entity<Movie>().HasKey(s => s.Id);
modelBuilder.Entity<Movie>()
.HasOne(e => e.ProductionCompany_R)
.WithMany(e => e.Movie_R)
.HasForeignKey(e => e.ProductionCompanyId)
.OnDelete(DeleteBehavior.Cascade);
// MovieDetail
modelBuilder.Entity<MovieDetail>().HasKey(s => s.MovieId);
modelBuilder.Entity<MovieDetail>()
.HasOne(e => e.Movie_R)
.WithOne(e => e.MovieDetail_R)
.HasForeignKey<MovieDetail>(e => e.MovieId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MovieDetail>(entity =>
{
entity.Property(e => e.MovieId)
.IsRequired();
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50);
entity.Property(e => e.Poster)
.IsRequired();
entity.Property(e => e.Budget)
.IsRequired()
.HasColumnType("Money"); ;
entity.Property(e => e.Gross)
.IsRequired();
entity.Property(e => e.ReleaseDate)
.IsRequired()
.HasColumnType("Date");
});
// Distribution
modelBuilder.Entity<Distribution>().HasKey(s => s.Id);
modelBuilder.Entity<Distribution>(entity =>
{
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50);
entity.Property(e => e.Location)
.IsRequired();
entity.Property(e => e.Telephone)
.IsRequired();
});
// MovieDistribution
modelBuilder.Entity<MovieDistribution>().HasKey(t => new { t.MovieId, t.DistributionId});
modelBuilder.Entity<MovieDistribution>()
.HasOne(t => t.Movie_R)
.WithMany(t => t.MovieDistribution_R)
.HasForeignKey(t => t.MovieId);
modelBuilder.Entity<MovieDistribution>()
.HasOne(t => t.Distribution_R)
.WithMany(t => t.MovieDistribution_R)
.HasForeignKey(t => t.DistributionId);
}
}
}
Inside the OnModelCreating method we have used Entity Framework Core Fluent APIs to configure models for database mappings, Primary Key, Foreign Key and Relationships.
For the Movie Entity we have set it’s primary key like:
modelBuilder.Entity<Movie>().HasKey(s => s.Id);
Then we have configured it’s One-to-Many Relationship with the MovieDetail entity along with setting the Foreign Key (i.e ‘ProductionCompanyId’) and Cascade Delete behaviour like:
modelBuilder.Entity<Movie>()
.HasOne(e => e.ProductionCompany_R)
.WithMany(e => e.Movie_R)
.HasForeignKey(e => e.ProductionCompanyId)
.OnDelete(DeleteBehavior.Cascade);
The Movie entity field called ProductionCompanyId will serve as the Foreign Key for the ProductionComany entity. With the OnDelete() method we have set the DeleteBehavior setting to Cascade. This means when the Parent entity i.e. here ‘ProductionCompany’ database table record is deleted then all it’s child records in the Movie table are also deleted.
Another thing to note is the configurations done for the MovieDistribution entity. We have first created a Composite Primary Key for it’s 2 fields which are MovieId and DistributionId like:
modelBuilder.Entity<MovieDistribution>().HasKey(t => new { t.MovieId, t.DistributionId});
Next we have created Many-to-Many Relationship between Distribution and Movie entities like:
modelBuilder.Entity<MovieDistribution>()
.HasOne(t => t.Movie_R)
.WithMany(t => t.MovieDistribution_R)
.HasForeignKey(t => t.MovieId);
modelBuilder.Entity<MovieDistribution>()
.HasOne(t => t.Distribution_R)
.WithMany(t => t.MovieDistribution_R)
.HasForeignKey(t => t.DistributionId);
Next add the below given Database Connection String in the appsettings.json file. Our database name will be “MovieDB” which you can change if you wish to.
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=MovieDB;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
}
}
Now register the Database Context which is the MovieContext.cs as a service in the Startup.cs class. The code which does this thing is:
services.AddDbContext<MovieContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
The full changes that need to be done are listed in the below code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xaero.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace Xaero
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//…
}
}
}
The changed are needed since we are fetching the connection string from the appsettings.json file. The recommended tutorial to read is Storing DB Connection String in appsettings.json.
Now it’s time to perform Entity Framework Core Migrations in order to create the database. In the Package Manager Console run the following 2 commands one by one.
PM> dotnet ef migrations add Migration1
PM> dotnet ef database update
When the migration commands finish executing, they will create the database along with the tables for the entities. Verfy the database in the SQL Server Object Explorer. See the below image which showns this database along with it’s tables.
We will be using Bootstrap framework for styling the Views to a proper design. Therefore we will need to Install Bootstrap from LibMan inside the wwwroot/lib/bootstrap folder.
Create _Layout.cshtml razor view file inside the Views ➤ Shared folder. It’s code is:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="~/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="m-1 p-1">
@RenderBody()
</div>
</body>
</html>
Next add _ViewStart.cshtml file inside the Views folder to specify the use of a common layout file for all views.
@{
Layout = "_Layout";
}
Finally add _ViewImports.cshtml file inside the Views folder to import the Models namespace and Built-In Tag Helpers. The code is given below.
@using Xaero.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
We will start with the creation of Production Company features which will include CRUD operation like Creation, Reading, Updation and Deletion of records. We will have 4 actions for performing each of these things.
Here we will create a feature to create Production Company records in the database using Entity Framework Core. The working of this feature is shown in the below given video:
So start by adding a new controller called ProductionController inside the Controllers folder of your project.
In this controller provide 2 objects with dependency injection. These objects are:
Also add the Create action method which does the actual creation of the production companies records. We have shown all these in the codes which are highlighted (see below).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.IO;
namespace Xaero.Controllers
{
public class ProductionController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public ProductionController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create(ProductionCompany pc, IFormFile Logo)
{
if (Logo == null)
ModelState.AddModelError(nameof(pc.Logo), "Please select logo file");
if (ModelState.IsValid)
{
string path = "Images/Production/" + Logo.FileName;
using (var stream = new FileStream(Path.Combine(hostingEnvironment.WebRootPath, path), FileMode.Create))
{
await Logo.CopyToAsync(stream);
}
var productionCompany = new ProductionCompany()
{
Name = pc.Name,
Logo = "~/" + path,
AnnualRevenue = pc.AnnualRevenue,
EstablishmentDate = pc.EstablishmentDate
};
context.Add(productionCompany);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
else
return View();
}
public IActionResult Index()
{
return View();
}
}
}
Points to be noted:
1. The Logo of the Production Company is uploaded to the Images/Production folder which is kept inside the wwwroot folder.
2. The parameter of the create action is IFormFile Logo. It will get the logo image from submitted from the view through the technique of Model Binding.
3. The EF Core codes that insert the record in the database is:
var productionCompany = new ProductionCompany()
{
Name = pc.Name,
Logo = "~/" + path,
AnnualRevenue = pc.AnnualRevenue,
EstablishmentDate = pc.EstablishmentDate
};
context.Add(productionCompany);
await context.SaveChangesAsync();
4. Once the insert has been completed the redirection to the Index action is performed.
Finally add the Create view inside the Views ➤ Production folder with the following code:
@model ProductionCompany
@{
ViewData["Title"] = "Create a Production";
}
<h1 class="bg-info text-white">Create a Production</h1>
<a asp-action="Index" class="btn btn-secondary">View all Productions</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Logo"></label>
<input type="file" asp-for="Logo" class="form-control" />
<span asp-validation-for="Logo" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AnnualRevenue"></label>
<input type="text" asp-for="AnnualRevenue" class="form-control" />
<span asp-validation-for="AnnualRevenue" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EstablishmentDate"></label>
<input type="date" asp-for="EstablishmentDate" class="form-control" />
<span asp-validation-for="EstablishmentDate" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
Now create the wwwroot/Images/Production folder where the Production Companies Logo files will be uploaded.
It’s time to see how the Create View looks so run the project and go to the Create View’s URL which in our case is https://localhost:44313/Production/Create. It will look like shown in the below image:
Now we will create the Read Feature for Production Companies. We will use the Index Action to perform this task. Users will be able to see all the Production Companies Records on the view plus there will be features like:
The full working of this Read Production Company feature is shown by the below video:
Note that the URL of the first page will be – https://localhost:44313/Production, the second page url will be – https://localhost:44313/Production/2. Similarly the 10’th page url will be https://localhost:44313/Production/10 and so on.
To create this feature we have to do 8 changes in total. Let us go through them one by one.
Add the Index action method to the ProductionController.cs file. The code is shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.IO;
using Xaero.Infrastructure;
namespace Xaero.Controllers
{
public class ProductionController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public ProductionController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
// other actions
public IActionResult Index(int id)
{
int pageSize = 3;
PagingInfo pagingInfo = new PagingInfo();
pagingInfo.CurrentPage = id == 0 ? 1 : id;
pagingInfo.TotalItems = context.ProductionCompany.Count();
pagingInfo.ItemsPerPage = pageSize;
var skip = pageSize * (Convert.ToInt32(id) - 1);
ViewBag.PagingInfo = pagingInfo;
var pc = context.ProductionCompany.Skip(skip).Take(pageSize).ToList();
return View(pc);
}
}
}
Points to note:
var skip = pageSize * (Convert.ToInt32(id) - 1);
var pc = context.ProductionCompany.Skip(skip).Take(pageSize).ToList();
Notice we have used the Skip() and Take() methods of LINQ to fetch the current page records only. This will make the feature lighter and faster at the same time.
As discussed earlier, the work of this Paging class is to contain the paging information. This class’s object will be set with paging values in the action method and is then send to the view through ViewBag variable.
So create a new folder called Infrastructure on the root of the project and create a new class called PagingInfo.cs inside it. The class code it given below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Xaero.Infrastructure
{
public class PagingInfo
{
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages
{
get
{
return (int)Math.Ceiling((decimal)TotalItems /
ItemsPerPage);
}
}
}
}
Next we need to create the Index view inside the Views ➤ Production folder. It’s full code is shown below:
@model List<ProductionCompany>
@{
ViewData["Title"] = "Production Companies";
}
<h1 class="bg-info text-white">Production Companies</h1>
<a asp-action="Create" class="btn btn-secondary">Create Production</a>
<table class="table table-sm table-bordered">
<tr>
<th>Id</th>
<th>Name</th>
<th>Logo</th>
<th>Annual Revenue</th>
<th>Establishment Date</th>
<th>Update</th>
<th>Delete</th>
</tr>
@foreach (ProductionCompany pc in Model)
{
<tr>
<td>@pc.Id</td>
<td>@pc.Name</td>
<td><img src="@Url.Content(pc.Logo)" /></td>
<td>@pc.AnnualRevenue.ToString("F2")</td>
<td>@pc.EstablishmentDate.ToString("d")</td>
<td>
<a class="btn btn-sm btn-primary" asp-action="Update" asp-route-id="@pc.Id">
Update
</a>
</td>
<td>
<form asp-action="Delete" asp-route-id="@pc.Id" method="post">
<button type="submit" class="btn btn-sm btn-danger">
Delete
</button>
</form>
</td>
</tr>
}
</table>
<div class="pagingDiv" page-model="@ViewBag.PagingInfo" page-action="Index" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div>
This view receives list of production companies in it’s model and then loops through them and show them inside an HTML table. The last line of code shows the creation of Paging Links by a Custom Tag Helper and consuming the paging information that the ViewBag variable is having.
<div class="pagingDiv" page-model="@ViewBag.PagingInfo" page-action="Index" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div>
Create a class called PageLinkTagHelper.cs inside the Infrastructure folder. Next add the below given code for creating a Custom Tag Helper.
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Collections.Generic;
using Microsoft.AspNetCore.Routing;
using System.Dynamic;
namespace Xaero.Infrastructure
{
[HtmlTargetElement("div", Attributes = "page-model")]
public class PageLinkTagHelper : TagHelper
{
private IUrlHelperFactory urlHelperFactory;
public PageLinkTagHelper(IUrlHelperFactory helperFactory)
{
urlHelperFactory = helperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
/*Accepts all attributes that are page-other-* like page-other-category="@Model.allTotal" page-other-some="@Model.allTotal"*/
[HtmlAttributeName(DictionaryAttributePrefix = "page-other-")]
public Dictionary<string, object> PageOtherValues { get; set; } = new Dictionary<string, object>();
public PagingInfo PageModel { get; set; }
public string PageAction { get; set; }
public bool PageClassesEnabled { get; set; } = false;
public string PageClass { get; set; }
public string PageClassNormal { get; set; }
public string PageClassSelected { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder("div");
string anchorInnerHtml = "";
for (int i = 1; i <= PageModel.TotalPages; i++)
{
TagBuilder tag = new TagBuilder("a");
anchorInnerHtml = AnchorInnerHtml(i, PageModel);
if (anchorInnerHtml == "..")
tag.Attributes["href"] = "#";
else if (PageOtherValues.Keys.Count != 0)
tag.Attributes["href"] = urlHelper.Action(PageAction, AddDictionaryToQueryString(i));
else
tag.Attributes["href"] = urlHelper.Action(PageAction, new { id = i });
if (PageClassesEnabled)
{
tag.AddCssClass(PageClass);
tag.AddCssClass(i == PageModel.CurrentPage ? PageClassSelected : "");
}
tag.InnerHtml.Append(anchorInnerHtml);
if (anchorInnerHtml != "")
result.InnerHtml.AppendHtml(tag);
}
output.Content.AppendHtml(result.InnerHtml);
}
public IDictionary<string, object> AddDictionaryToQueryString(int i)
{
object routeValues = null;
var dict = (routeValues != null) ? new RouteValueDictionary(routeValues) : new RouteValueDictionary();
dict.Add("id", i);
foreach (string key in PageOtherValues.Keys)
{
dict.Add(key, PageOtherValues[key]);
}
var expandoObject = new ExpandoObject();
var expandoDictionary = (IDictionary<string, object>)expandoObject;
foreach (var keyValuePair in dict)
{
expandoDictionary.Add(keyValuePair);
}
return expandoDictionary;
}
public static string AnchorInnerHtml(int i, PagingInfo pagingInfo)
{
string anchorInnerHtml = "";
if (pagingInfo.TotalPages <= 10)
anchorInnerHtml = i.ToString();
else
{
if (pagingInfo.CurrentPage <= 5)
{
if ((i <= 8) || (i == pagingInfo.TotalPages))
anchorInnerHtml = i.ToString();
else if (i == pagingInfo.TotalPages - 1)
anchorInnerHtml = "..";
}
else if ((pagingInfo.CurrentPage > 5) && (pagingInfo.TotalPages - pagingInfo.CurrentPage >= 5))
{
if ((i == 1) || (i == pagingInfo.TotalPages) || ((pagingInfo.CurrentPage - i >= -3) && (pagingInfo.CurrentPage - i <= 3)))
anchorInnerHtml = i.ToString();
else if ((i == pagingInfo.CurrentPage - 4) || (i == pagingInfo.CurrentPage + 4))
anchorInnerHtml = "..";
}
else if (pagingInfo.TotalPages - pagingInfo.CurrentPage < 5)
{
if ((i == 1) || (pagingInfo.TotalPages - i <= 7))
anchorInnerHtml = i.ToString();
else if (pagingInfo.TotalPages - i == 8)
anchorInnerHtml = "..";
}
}
return anchorInnerHtml;
}
}
}
Points to note:
[HtmlTargetElement("div", Attributes = "page-model")]
applied to it says that the tag helper can be applied on only an html div element and must have the attribute called page-model.public PagingInfo PageModel { get; set; }
. Similarly the page-action value is received by the PageAction property and so on.Register this Custom Tag Helper inside the View Imports file. The below highlighted code line is the one which we have to add to the _ViewImports.cshtml file.
@using Xaero.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper Xaero.Infrastructure.PageLinkTagHelper, Xaero
We will need to add some CSS for the paging links. We can do this by adding the CSS directly to the _Layout.cshtml file. Check the below highlighted code of the layout file which shows this CSS.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="~/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<style>
table img {
width: 200px;
height: 100px;
}
.pagingDiv {
background: #f2f2f2;
}
.pagingDiv > a {
display: inline-block;
padding: 0px 9px;
margin-right: 4px;
border-radius: 3px;
border: solid 1px #c0c0c0;
background: #e9e9e9;
box-shadow: inset 0px 1px 0px rgba(255,255,255, .8), 0px 1px 3px rgba(0,0,0, .1);
font-size: .875em;
font-weight: bold;
text-decoration: none;
color: #717171;
text-shadow: 0px 1px 0px rgba(255,255,255, 1);
}
.pagingDiv > a:hover {
background: #fefefe;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FEFEFE), to(#f0f0f0));
background: -moz-linear-gradient(0% 0% 270deg,#FEFEFE, #f0f0f0);
}
.pagingDiv > a.active {
border: none;
background: #616161;
box-shadow: inset 0px 0px 8px rgba(0,0,0, .5), 0px 1px 0px rgba(255,255,255, .8);
color: #f0f0f0;
text-shadow: 0px 0px 3px rgba(0,0,0, .5);
}
</style>
</head>
<body>
<div class="m-1 p-1">
@RenderBody()
</div>
</body>
</html>
Finally in the Startup class the relevant routes need to be added. These routes will create seo friendly urls that contains page number as the last segment. Eg –
https://localhost:44313/Production
https://localhost:44313/Production/1
https://localhost:44313/Production/2
https://localhost:44313/Production/10
So go to the Startup.cs file and add the below highlighted codes inside the app.UseEndpoints() method:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "PagingPageOne",
pattern: "{controller}",
defaults: new { action = "Index", id = 1 });
endpoints.MapControllerRoute(
name: "Paging",
pattern: "{controller}/{id:int?}",
defaults: new { action = "Index" });
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
I have written 2 useful tutorials on Routing which are:
There should be a way for users to sort production records in ascending or descending manner. So we are going to add this feature. Unlike other tutorials where you see the sort information send to the url in query string like this :
https://localhost:44313/Production?Name=asc
https://localhost:44313/Production/1?Name=desc
https://localhost:44313/Production/2?Annual Revenue=asc
We will use another approach which will not use the query string, instead we use cookies to send the sort information to the server. So here the url will remain totally unaffected.
Start by changing the Index action method of the Production Controller file as shown below:
public IActionResult Index(int id)
{
string cookieValueFromReq = Request.Cookies["sortCookie"];
List<ProductionCompany> pcList;
if (string.IsNullOrEmpty(cookieValueFromReq))
pcList = GetRecords(id);
else
{
string sortColumn = cookieValueFromReq.Split(',')[0];
string sortValue = cookieValueFromReq.Split(',')[1];
pcList = GetRecords(id, sortColumn, sortValue);
}
return View(pcList);
}
Notice we are first requesting “Cookie” named sortCookie.
string cookieValueFromReq = Request.Cookies["sortCookie"];
This cookie will contain the name of the “column” and “sort value” in comma separated manner. For example the value – Id, asc means sort by Id column in ascending manner. Similarly Id, desc means sort by Id column in descending manner.
In the same way the value Name, desc means to sort by Name column in descending manner.
Next we are calling the GetRecords() function which will be fetching the sorted records from the database by Entity Framework Core.
So add this GetRecords() function to the Production Controller.
List<ProductionCompany> GetRecords(int page, string sortColumn = "", string sortValue = "")
{
int pageSize = 3;
PagingInfo pagingInfo = new PagingInfo();
pagingInfo.CurrentPage = page == 0 ? 1 : page;
pagingInfo.TotalItems = context.ProductionCompany.Count();
pagingInfo.ItemsPerPage = pageSize;
var skip = pageSize * (Convert.ToInt32(page) - 1);
ViewBag.PagingInfo = pagingInfo;
List<ProductionCompany> result;
if (sortColumn == "")
result = context.ProductionCompany.Skip(skip).Take(pageSize).ToList();
else
{
if (sortValue == "asc")
{
switch (sortColumn)
{
case "Id":
result = context.ProductionCompany.OrderBy(s => s.Id).Skip(skip).Take(pageSize).ToList();
break;
case "Name":
result = context.ProductionCompany.OrderBy(s => s.Name).Skip(skip).Take(pageSize).ToList();
break;
case "Annual Revenue":
result = context.ProductionCompany.OrderBy(s => s.AnnualRevenue).Skip(skip).Take(pageSize).ToList();
break;
case "Establishment Date":
result = context.ProductionCompany.OrderBy(s => s.EstablishmentDate).Skip(skip).Take(pageSize).ToList();
break;
default:
result = context.ProductionCompany.OrderBy(s => s.Name).Skip(skip).Take(pageSize).ToList();
break;
}
}
else
{
switch (sortColumn)
{
case "Id":
result = context.ProductionCompany.OrderByDescending(s => s.Id).Skip(skip).Take(pageSize).ToList();
break;
case "Name":
result = context.ProductionCompany.OrderByDescending(s => s.Name).Skip(skip).Take(pageSize).ToList();
break;
case "Annual Revenue":
result = context.ProductionCompany.OrderByDescending(s => s.AnnualRevenue).Skip(skip).Take(pageSize).ToList();
break;
case "Establishment Date":
result = context.ProductionCompany.OrderByDescending(s => s.EstablishmentDate).Skip(skip).Take(pageSize).ToList();
break;
default:
result = context.ProductionCompany.OrderByDescending(s => s.Name).Skip(skip).Take(pageSize).ToList();
break;
}
}
}
return result;
}
Now it’s time to change the Index View. Here we only have to add the class="sort"
attribute to the columns we want to be sorted. We have added it to the 4 columns which are –
Check the highlighted code of the Index view which shows these changes:
@model List<ProductionCompany>
@{
ViewData["Title"] = "Production Companies";
}
<h1 class="bg-info text-white">Production Companies</h1>
<a asp-action="Create" class="btn btn-secondary">Create Production</a>
<table class="table table-sm table-bordered">
<tr>
<th class="sort">Id</th>
<th class="sort">Name</th>
<th>Logo</th>
<th class="sort">Annual Revenue</th>
<th class="sort">Establishment Date</th>
<th>Update</th>
<th>Delete</th>
</tr>
@foreach (ProductionCompany pc in Model)
{
<tr>
<td>@pc.Id</td>
<td>@pc.Name</td>
<td><img src="@Url.Content(pc.Logo)" /></td>
<td>@pc.AnnualRevenue.ToString("F2")</td>
<td>@pc.EstablishmentDate.ToString("d")</td>
<td>
<a class="btn btn-sm btn-primary" asp-action="Update" asp-route-id="@pc.Id">
Update
</a>
</td>
<td>
<form asp-action="Delete" asp-route-id="@pc.Id" method="post">
<button type="submit" class="btn btn-sm btn-danger">
Delete
</button>
</form>
</td>
</tr>
}
</table>
<div class="pagingDiv" page-model="@ViewBag.PagingInfo" page-action="Index" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div>
Finally we will need to add the JavaScript Code which does the cookie creation and reading part that forms the core logic of this sorting feature by cookie method.
We will add this JavaScript code to the _Layout.cshtml file as it is the common layout for every view. So all views will share this script.
We have highlighted the changes to be made to this _Layout.cshtml file. This includes the JavaScript code along with a small CSS which changes the cursor to pointer when mouse is hovered over any column that supports sorting.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="~/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<style>
table tr th.sort:hover {
cursor: pointer;
}
table tr td img {
width: 200px;
height: 100px;
}
.pagingDiv {
background: #f2f2f2;
}
.pagingDiv > a {
display: inline-block;
padding: 0px 9px;
margin-right: 4px;
border-radius: 3px;
border: solid 1px #c0c0c0;
background: #e9e9e9;
box-shadow: inset 0px 1px 0px rgba(255,255,255, .8), 0px 1px 3px rgba(0,0,0, .1);
font-size: .875em;
font-weight: bold;
text-decoration: none;
color: #717171;
text-shadow: 0px 1px 0px rgba(255,255,255, 1);
}
.pagingDiv > a:hover {
background: #fefefe;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FEFEFE), to(#f0f0f0));
background: -moz-linear-gradient(0% 0% 270deg,#FEFEFE, #f0f0f0);
}
.pagingDiv > a.active {
border: none;
background: #616161;
box-shadow: inset 0px 0px 8px rgba(0,0,0, .5), 0px 1px 0px rgba(255,255,255, .8);
color: #f0f0f0;
text-shadow: 0px 0px 3px rgba(0,0,0, .5);
}
</style>
</head>
<body>
<div class="m-1 p-1">
@RenderBody()
</div>
<script>
var dth = document.getElementsByTagName("table").item(0).getElementsByClassName("sort");
var cVal = decodeURIComponent(getCookie("sortCookie"));
var nColOrder = "";
if (cVal != "null") {
var colName = cVal.split(',')[0];
var colOrder = cVal.split(',')[1];
var colClicked = Array.from(document.querySelectorAll("th")).find(el => el.innerText == colName);
if (colOrder == "asc")
colClicked.innerHTML += "<img src=\"/Images/up.png\" />";
else
colClicked.innerHTML += "<img src=\"/Images/down.png\" />";
}
for (let i = 0; i < dth.length; i++) {
dth[i].addEventListener("click", function (e) {
if (cVal != null) {
var colName = cVal.split(',')[0];
var colOrder = cVal.split(',')[1];
var clickedColumn = this.innerText.replace(/<[^>]*>?/gm, '');
if (clickedColumn == colName) {
if (colOrder == "asc")
nColOrder = "desc";
else
nColOrder = "asc";
setCookie("sortCookie", clickedColumn + "," + nColOrder);
}
else {
setCookie("sortCookie", clickedColumn + ",asc");
}
}
else {
setCookie("sortCookie", clickedColumn + ",asc");
}
var cUrl = window.location.href;
if(cUrl.includes("Production"))
window.location = "@Url.Action("Index","Production")";
else if(cUrl.includes("Movie"))
window.location = "@Url.Action("Index","Movie")";
else if(cUrl.includes("Distribution"))
window.location="@Url.Action("Index","Distribution")";
});
}
function setCookie(name, value) {
var expires = new Date();
expires.setTime(expires.getTime() + (1 * 60 * 60 * 1000)); // adding 1 hour
document.cookie = name + '=' + encodeURIComponent(value) + ';path=/;expires=' + expires.toUTCString();
}
function getCookie(name) {
var value = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return value ? value[2] : null;
}
</script>
</body>
</html>
Points to note:
if (colOrder == "asc")
colClicked.innerHTML += "<img src=\"/Images/up.png\" />";
else
colClicked.innerHTML += "<img src=\"/Images/down.png\" />";
The “up.png” & “down.png” images are added to the wwwroot/Images folder. These images are provided on the source code of this project.
Check the below video which shown how sorting works:
The Update Action of the Production Controller will do the updation of the records of the Production Companies. The Update Action method code is given below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.IO;
using Xaero.Infrastructure;
namespace Xaero.Controllers
{
public class ProductionController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public ProductionController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
// removed for brevity
public IActionResult Update(int id)
{
var pc = context.ProductionCompany.Where(a => a.Id == id).FirstOrDefault();
return View(pc);
}
[HttpPost]
public async Task<IActionResult> Update(ProductionCompany pc, IFormFile mLogo)
{
if (ModelState.IsValid)
{
string path = pc.Logo;
if (mLogo != null)
{
path = "Images/Production/" + mLogo.FileName;
using (var stream = new FileStream(Path.Combine(hostingEnvironment.WebRootPath, path), FileMode.Create))
{
await mLogo.CopyToAsync(stream);
}
}
var productionCompany = new ProductionCompany()
{
Id = pc.Id,
Name = pc.Name,
Logo = path.Substring(0, 2) == "~/" ? path : "~/" + path,
AnnualRevenue = pc.AnnualRevenue,
EstablishmentDate = pc.EstablishmentDate
};
context.Update(productionCompany);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
else
return View(pc);
}
}
}
Points to note:
context.Update(productionCompany);
await context.SaveChangesAsync();
Now add the Update View inside the Views ➤ Production folder containing the codes as given below:
@model ProductionCompany
@{
ViewData["Title"] = "Update a Production";
}
<h1 class="bg-info text-white">Update a Production</h1>
<a asp-action="Index" class="btn btn-secondary">View all Productions</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="Id"></label>
<input type="text" asp-for="Id" disabled class="form-control" />
</div>
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Logo"></label>
<img src="@Url.Content(Model.Logo)" />
<input type="text" asp-for="Logo" hidden />
<input type="file" id="mLogo" name="mLogo" class="form-control" />
</div>
<div class="form-group">
<label asp-for="AnnualRevenue"></label>
<input type="text" asp-for="AnnualRevenue" class="form-control" />
<span asp-validation-for="AnnualRevenue" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EstablishmentDate"></label>
<input type="date" asp-for="EstablishmentDate" class="form-control" />
<span asp-validation-for="EstablishmentDate" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
Notice the current logo is bind to the img tag as –
<img src="@Url.Content(Model.Logo)" />
And the logo path to the input tag as.
<input type="text" asp-for="Logo" hidden />
The new logo will be bind to the input tag called –
<input type="file" id="mLogo" name="mLogo" class="form-control" />
And when the form is posted then the new logo file will reach the IFormFile type argument of the update action method. The action in turn will upload this new file as the new logo for the company.
Check the below video which shows this update feature.
Notice that we have given a Delete button on the Index action method. On clicking the delete button the corresponding record gets deleted. So add the Delete action method to the Production controller. It’s code is highlighted (see below).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.IO;
using Xaero.Infrastructure;
namespace Xaero.Controllers
{
public class ProductionController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public ProductionController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
// removed for brevity
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
var pc = context.ProductionCompany.Where(a => a.Id == id).FirstOrDefault();
context.Remove(pc);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
}
}
The delete action gets the id of the record to be deleted in it’s parameter and then uses Entity Framework Core to delete it from the database. The below 2 codes lines does the actual deletion of the record:
context.Remove(pc);
await context.SaveChangesAsync();
Check below video to see it’s working:
The Movie features includes the CRUD operation like Creation, Reading, Updation and Deletion of Movie records. We will have 4 actions for performing each of these things. Note that their will be One-to-Many relationship between Production and Movie entities, this means 1 production company can have many movies associated with it.
Another relationship is a One-to-One relationship which the Movie entity has with the MovieDetail entity.
First create a new controller called MovieController.cs inside the Controllers folder. Next provide it’s constructor with the MovieContext and IWebHostEnvironment objects. See highlighted code below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using Xaero.Models;
namespace Xaero.Controllers
{
public class MovieController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public MovieController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
public IActionResult Index()
{
return View();
}
}
}
Now add a new action method called Create to the MovieController whose code is shown below in highlighted manner.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using Xaero.Models;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Xaero.Controllers
{
public class MovieController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public MovieController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
public IActionResult Index()
{
return View();
}
public IActionResult Create()
{
GetProduction();
return View();
}
[HttpPost]
public async Task<IActionResult> Create(Movie movie, IFormFile Poster)
{
GetProduction();
if (Poster == null)
ModelState.AddModelError("MovieDetail_R.Poster", "Please select Movie Poster");
if (ModelState.IsValid)
{
string path = "Images/Movie/" + Poster.FileName;
using (var stream = new FileStream(Path.Combine(hostingEnvironment.WebRootPath, path), FileMode.Create))
{
await Poster.CopyToAsync(stream);
}
var movieDetail = new MovieDetail()
{
Name = movie.MovieDetail_R.Name,
Poster = "~/" + path,
Budget = movie.MovieDetail_R.Budget,
Gross = movie.MovieDetail_R.Gross,
ReleaseDate = movie.MovieDetail_R.ReleaseDate
};
var m = new Movie()
{
ProductionCompanyId = movie.ProductionCompanyId,
MovieDetail_R = movieDetail
};
context.Add(m);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
else
return View();
}
void GetProduction()
{
List<SelectListItem> production = new List<SelectListItem>();
production = context.ProductionCompany.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }).ToList();
ViewBag.Production = production;
}
}
}
Points to note:
1. This action method calls a function – GetProduction() which fetches all the Production Company records from the database and adds them to a List<SelectListItem> object. Finally this object is added to a ViewBag variable. The ViewBag variable’s value, which is of type List<SelectListItem>, will be shown inside a select html control on the view.
2. The second parameter of the action – IFormFile Poster will get the movie poster uploaded from the view and is stored inside the wwwroot/Images/Movie folder (so create Movie folder on the project). The saving part is done by the below code:
string path = "Images/Movie/" + Poster.FileName;
using (var stream = new FileStream(Path.Combine(hostingEnvironment.WebRootPath, path), FileMode.Create))
{
await Poster.CopyToAsync(stream);
}
3. The Movie movie parameter receive the movie details filled by the user on the view.
We created 2 objects which are – MovieDetail & Movie.
var movieDetail = new MovieDetail()
{…}
var m = new Movie()
{…}
We provide the values to the properties of the MovieDetail object, and then provide the value to the corresponding Reference Navigation Property of the Movie object. That is, we set the MovieDetail_R property value to be the value of the MovieDetail object. Check the below code:
var m = new Movie()
{
ProductionCompanyId = movie.ProductionCompanyId,
MovieDetail_R = movieDetail
};
Other than this, we also set ProductionCompanyId property value of the Movie object which is received on the parameter of the action method – ProductionCompanyId = movie.ProductionCompanyId.
Finally the movie record is created by the below given code lines:
context.Add(m);
await context.SaveChangesAsync();
This is the way of “Inserting Related Records on the Database with EF Core”. You can check this tutorial for complete information.
You also need to add the Create View inside the Views ➤ Movie folder and having the following codes:
@model Movie
@{
ViewData["Title"] = "Create a Movie";
}
<h1 class="bg-info text-white">Create a Movie</h1>
<a asp-action="Index" class="btn btn-secondary">View all Movies</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.Name"></label>
<input type="text" asp-for="@Model.MovieDetail_R.Name" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductionCompanyId">Production Company</label>
<select asp-for="ProductionCompanyId" asp-items="ViewBag.Production" class="form-control">
</select>
</div>
<div class="form-group">
<label for="Poster"></label>
<input type="file" id="Poster" name="Poster" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.Poster" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.Budget"></label>
<input type="text" asp-for="@Model.MovieDetail_R.Budget" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.Gross"></label>
<input type="text" asp-for="@Model.MovieDetail_R.Gross" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.Gross" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.ReleaseDate"></label>
<input type="date" asp-for="@Model.MovieDetail_R.ReleaseDate" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.ReleaseDate" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
The only important thing to be noted here is how I am binding the fields of the MovieDetail object by going through the Reference Navigation property like @Model.MovieDetail_R.Name for the “Name” property of the MovieDetail entity. Check the below code:
<input type="text" asp-for="@Model.MovieDetail_R.Name" class="form-control" />
Movie records can be added from the url https://localhost:44313/Movie/Create and it’s screenshot is shown below.
The read movie feature will be provided by the Index action and Index view. So update the Index view code to as shown below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using Xaero.Models;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Xaero.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Xaero.Controllers
{
public class MovieController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public MovieController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
public IActionResult Index(int id)
{
int pageSize = 3;
PagingInfo pagingInfo = new PagingInfo();
pagingInfo.CurrentPage = id == 0 ? 1 : id;
pagingInfo.TotalItems = context.Movie.Count();
pagingInfo.ItemsPerPage = pageSize;
var skip = pageSize * (Convert.ToInt32(id) - 1);
ViewBag.PagingInfo = pagingInfo;
var movies = context.Movie.Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
return View(movies);
}
// removed for brevity
}
}
The action method receives the current page no in it’s parameter and fetches only the records that comes under that page from the database. For this it uses the Skip & Take methods of LINQ.
We can see that in this action we are fetching the Movie records from the database along with the related MovieDetail and ProductionCompany records also. We use the Include() method of Entity Framework Core for doing this thing.
var movies = context.Movie.Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
Next add the Index View inside the Views ➤ Movie folder and having the below code:
@model List<Movie>
@{
ViewData["Title"] = "Movie";
}
<h1 class="bg-info text-white">Movies</h1>
<a asp-action="Create" class="btn btn-secondary">Create Movie</a>
<table class="table table-sm table-bordered">
<tr>
<th class="sort">Id</th>
<th class="sort">Name</th>
<th>Production Company</th>
<th>Poster</th>
<th class="sort">Budget</th>
<th class="sort">Gross</th>
<th class="sort">Release Date</th>
<th>Movie Distribution</th>
<th>Update</th>
<th>Delete</th>
</tr>
@foreach (Movie movie in Model)
{
<tr>
<td>@movie.Id</td>
<td>@movie.MovieDetail_R.Name</td>
<td>@movie.ProductionCompany_R.Name</td>
<td><img src="@Url.Content(movie.MovieDetail_R.Poster)" /></td>
<td>@movie.MovieDetail_R.Budget.ToString("F2")</td>
<td>@movie.MovieDetail_R.Gross.ToString("F2")</td>
<td>@movie.MovieDetail_R.ReleaseDate.ToString("d")</td>
<td>
<a class="btn btn-sm btn-danger" asp-controller="MovieDistribution" asp-action="Update" asp-route-id="@movie.Id">
Movie Distribution
</a>
</td>
<td>
<a class="btn btn-sm btn-primary" asp-action="Update" asp-route-id="@movie.Id">
Update
</a>
</td>
<td>
<form asp-action="Delete" asp-route-id="@movie.Id" method="post">
<button type="submit" class="btn btn-sm btn-danger">
Delete
</button>
</form>
</td>
</tr>
}
</table>
<div class="pagingDiv" page-model="@ViewBag.PagingInfo" page-action="Index" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div>
Notice we am using Reference Navigation Properties to get the Production Company name as – @movie.ProductionCompany_R.Name and the Movie budget as @movie.MovieDetail_R.Budget.
The “div” given at the end will create the pagination feature.
Let’s create a new movie, see the below video which shows this procedure.
Now check the Read Movies feature, see below video.
Notice that right now there is not an option for sorting. So without any delay let us create the sorting feature.
Most of the Sorting infrastructure is already in place, we created it during the time of Production Company sorting. We will use the same infrastructure for Movie to. Recall, sorting infrastructure includes the following things:
class="sort"
to the sortable columns. We have added this class to “Id”, “Name”, “Budget”, “Gross” and “Release Date”.We now only have to update the Index action method to use this infrastructure. The highlighted code which is given below illustrates the changes that need to be made.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using Xaero.Models;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Xaero.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Xaero.Controllers
{
public class MovieController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public MovieController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
public IActionResult Index(int id)
{
string cookieValueFromReq = Request.Cookies["sortCookie"];
List<Movie> mList;
if (string.IsNullOrEmpty(cookieValueFromReq))
mList = GetRecords(id);
else
{
string sortColumn = cookieValueFromReq.Split(',')[0];
string sortValue = cookieValueFromReq.Split(',')[1];
mList = GetRecords(id, sortColumn, sortValue);
}
return View(mList);
}
List<Movie> GetRecords(int page, string sortColumn = "", string sortValue = "")
{
int pageSize = 3;
PagingInfo pagingInfo = new PagingInfo();
pagingInfo.CurrentPage = page == 0 ? 1 : page;
pagingInfo.TotalItems = context.Movie.Count();
pagingInfo.ItemsPerPage = pageSize;
var skip = pageSize * (Convert.ToInt32(page) - 1);
ViewBag.PagingInfo = pagingInfo;
List<Movie> result;
if (sortColumn == "")
result = context.Movie.Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
else
{
if (sortValue == "asc")
{
switch (sortColumn)
{
case "Id":
result = context.Movie.OrderBy(s => s.Id).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
case "Name":
result = context.Movie.OrderBy(s => s.MovieDetail_R.Name).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
case "Budget":
result = context.Movie.OrderBy(s => s.MovieDetail_R.Budget).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
case "Gross":
result = context.Movie.OrderBy(s => s.MovieDetail_R.Gross).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
case "Release Date":
result = context.Movie.OrderBy(s => s.MovieDetail_R.ReleaseDate).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
default:
result = context.Movie.OrderBy(s => s.MovieDetail_R.Name).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
}
}
else
{
switch (sortColumn)
{
case "Id":
result = context.Movie.OrderByDescending(s => s.Id).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
case "Name":
result = context.Movie.OrderByDescending(s => s.MovieDetail_R.Name).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
case "Budget":
result = context.Movie.OrderByDescending(s => s.MovieDetail_R.Budget).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
case "Gross":
result = context.Movie.OrderByDescending(s => s.MovieDetail_R.Gross).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
case "Release Date":
result = context.Movie.OrderByDescending(s => s.MovieDetail_R.ReleaseDate).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
default:
result = context.Movie.OrderByDescending(s => s.MovieDetail_R.Name).Skip(skip).Take(pageSize).Include(s => s.MovieDetail_R).Include(s => s.ProductionCompany_R).ToList();
break;
}
}
}
return result;
}
//…
}
}
The code is easy to understand, see the GetRecords() methods which fetches the records in sorted way. It uses switch statement to fetch the records based on the sort info that the cookie stores.
Now we can test it by going to the URL – https://localhost:44313/Movie. Click the column names to sort the records. Check the below given video which shows the sorting feature in action.
Create the Update action method which will perform the updation of movie records. We have given it’s code in highlighted way below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using Xaero.Models;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Xaero.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Xaero.Controllers
{
public class MovieController : Controller
{
private MovieContext context;
private IWebHostEnvironment hostingEnvironment;
public MovieController(MovieContext mc, IWebHostEnvironment environment)
{
context = mc;
hostingEnvironment = environment;
}
// … other actions
public IActionResult Update(int id)
{
var movie = context.Movie.Where(a => a.Id == id).Include(s => s.MovieDetail_R).FirstOrDefault();
GetProduction();
return View(movie);
}
[HttpPost]
public async Task<IActionResult> Update(Movie movie, IFormFile mPoster)
{
GetProduction();
if (ModelState.IsValid)
{
string path = movie.MovieDetail_R.Poster;
if (mPoster != null)
{
path = "Images/Production/" + mPoster.FileName;
using (var stream = new FileStream(Path.Combine(hostingEnvironment.WebRootPath, path), FileMode.Create))
{
await mPoster.CopyToAsync(stream);
}
}
var movieDetail = new MovieDetail()
{
MovieId = movie.Id,
Name = movie.MovieDetail_R.Name,
Poster = path.Substring(0, 2) == "~/" ? path : "~/" + path,
Budget = movie.MovieDetail_R.Budget,
Gross = movie.MovieDetail_R.Gross,
ReleaseDate = movie.MovieDetail_R.ReleaseDate
};
var m = new Movie()
{
Id = movie.Id,
ProductionCompanyId = movie.ProductionCompanyId,
MovieDetail_R = movieDetail
};
context.Update(m);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
else
return View(movie);
}
}
}
Points to note:
MovieDetail_R = movieDetail
to tell EF core about the related entity.Make sure to add the Update View inside the Views ➤ Movie folder and having the below code:
@model Movie
@{
ViewData["Title"] = "Update a Movie";
}
<h1 class="bg-info text-white">Update a Movie</h1>
<a asp-action="Index" class="btn btn-secondary">View all Movies</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="Id"></label>
<input type="text" asp-for="Id" disabled class="form-control" />
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.Name"></label>
<input type="text" asp-for="@Model.MovieDetail_R.Name" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductionCompanyId">Production Company</label>
<select asp-for="ProductionCompanyId" asp-items="ViewBag.Production" class="form-control">
</select>
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.Poster"></label>
<img src="@Url.Content(Model.MovieDetail_R.Poster)" width="500" />
<input type="text" asp-for="@Model.MovieDetail_R.Poster" hidden />
<input type="file" id="mPoster" name="mPoster" class="form-control" />
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.Budget"></label>
<input type="text" asp-for="@Model.MovieDetail_R.Budget" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.Gross"></label>
<input type="text" asp-for="@Model.MovieDetail_R.Gross" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.Gross" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.ReleaseDate"></label>
<input type="date" asp-for="@Model.MovieDetail_R.ReleaseDate" class="form-control" />
<span asp-validation-for="@Model.MovieDetail_R.ReleaseDate" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
See the video which shows the working of the update feature:
The Index view has a delete button which on clicking will initiate the Delete Action. Open the Index View to check this below given code.
<form asp-action="Delete" asp-route-id="@movie.Id" method="post">
<button type="submit" class="btn btn-sm btn-danger">
Delete
</button>
</form>
Next add the Delete action code to the controller. We have given it’s code:
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
var movie = context.Movie.Where(a => a.Id == id).FirstOrDefault();
context.Remove(movie);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
Also remember that on the DB Context file we have declared delete behaviour to be cascade.
.OnDelete(DeleteBehavior.Cascade);
This means if the Movie record is deleted then the related records from the MovieDetail table and MovieDistribution table are automatically deleted. Check this article for more info in this topic.
Now run your project and click on the delete button against any movie to delete it from the database. Check the below given video.
We created a MovieDistribution joining entity to create the Many-to-Many relationship between the “Movie” and “Distribution” entities. We recommend you to read this article to understand how this relationship is created in EF Core. So create a new controller called MovieDistribution inside the Controllers folder and here the necessary codes will be added.
This controller will have just one action method which is called Update action. In this action the Many-to-Many relationship will be set for the entities. For this the action will insert multiple records on the MovieDistribution table simultaneously to create this relationship.
We have shown this action method code below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Xaero.Controllers
{
public class MovieDistributionController : Controller
{
private MovieContext context;
public MovieDistributionController(MovieContext mc)
{
context = mc;
}
public IActionResult Update(int id)
{
GetMovieDistribution(id);
var movie = context.Movie.Where(a => a.Id == id).Include(s => s.MovieDetail_R).FirstOrDefault();
return View(movie);
}
[HttpPost]
public async Task<IActionResult> Update(Movie movie, string[] distribution)
{
GetMovieDistribution(movie.Id);
if (ModelState.IsValid)
{
context.RemoveRange(context.MovieDistribution.Where(t => t.MovieId == movie.Id).ToList());
List<MovieDistribution> mdList = new List<MovieDistribution>();
foreach (string d in distribution)
{
var md = new MovieDistribution()
{
MovieId = movie.Id,
DistributionId = Convert.ToInt32(d)
};
mdList.Add(md);
}
context.AddRange(mdList);
await context.SaveChangesAsync();
return RedirectToAction("Index", "Movie");
}
else
return View(movie);
}
void GetMovieDistribution(int movie)
{
List<SelectListItem> md = new List<SelectListItem>();
md = context.Distribution.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString(), Selected = x.MovieDistribution_R.Where(y => y.MovieId == movie).Any(z => z.DistributionId == x.Id) }).ToList();
ViewBag.MD = md;
}
}
}
Understanding the codes:
So the code which deletes the older records of the movie is:
context.RemoveRange(context.MovieDistribution.Where(t => t.MovieId == movie.Id).ToList());
Then the new records are inserted with the AddRange() method:
context.AddRange(mdList);
List<MovieDistribution> mdList = new List<MovieDistribution>();
foreach (string d in distribution)
{
var md = new MovieDistribution()
{
MovieId = movie.Id,
DistributionId = Convert.ToInt32(d)
};
mdList.Add(md);
}
The parameter of the action gets a string array containing all the distribution ids which are then added to the mdList variable by the foreach loop.
Now add the Update View inside the Views ➤ MovieDistribution folder and having the below code:
@model Movie
@{
ViewData["Title"] = "Create Movie Distribution";
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
<script>
$(document).ready(function () {
$("select").select2();
});
</script>
<h1 class="bg-info text-white">Create a Movie Distribution</h1>
<a asp-controller="Movie" asp-action="Index" class="btn btn-secondary">View all Movies</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="Id"></label>
<input type="text" asp-for="Id" disabled class="form-control" />
</div>
<div class="form-group">
<label asp-for="@Model.MovieDetail_R.Name"></label>
<input type="text" asp-for="@Model.MovieDetail_R.Name" disabled class="form-control" />
</div>
<div class="form-group">
<label for="Distribution"></label>
<select id="Distribution" name="Distribution" asp-items="ViewBag.MD" multiple class="form-control">
</select>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
In the view code we can see that it is receiving an object of Movie type as a model. The Select control is turned into a Select2 jQuery plugin. So the users will be able to select multiple Distribution values in the select control. This feature will help to greatly improve the working of the creation of the Many-to-Many relationship in just one go.
The select control is binded to the value of the ViewBag variable which is send by the controller:
<select id="Distribution" name="Distribution" asp-items="ViewBag.MD" multiple class="form-control"></select>
We will show the working video of this feature only after we have created the Distribution feature (given below). This is because the magic happens only there.
The Distribution will contain the Movie Distribution information. The Distribution entity has a Many-to-Many Relationship with the Movie entity. We have also used a Joining Entity called MovieDistribution.cs to create this relationship. Let us start by adding DistributionController to the Controllers folder where the whole logic will be created.
Add the Create action in the controller. The necessary codes are shown in highlighted manner.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
namespace Xaero.Controllers
{
public class DistributionController : Controller
{
private MovieContext context;
public DistributionController(MovieContext mc)
{
context = mc;
}
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create(Distribution distribution)
{
if (ModelState.IsValid)
{
context.Add(distribution);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
else
return View();
}
public IActionResult Index()
{
return View();
}
}
}
The code which does the record creation in the database is:
context.Add(distribution);
await context.SaveChangesAsync();
Now add the Create View inside the Views ➤ Distribution folder and having the below code:
@model Distribution
@{
ViewData["Title"] = "Create a Distribution";
}
<h1 class="bg-info text-white">Create a Distribution</h1>
<a asp-action="Index" class="btn btn-secondary">View all Distribution</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post">
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Location">Location</label>
<input type="text" asp-for="Location" class="form-control" />
<span asp-validation-for="Location" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Telephone"></label>
<input type="text" asp-for="Telephone" class="form-control" />
<span asp-validation-for="Telephone" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
Reading the Distribution records is done by the Index action method whose code is given below. Note that the records can also be sorted by the user.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
using Xaero.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Xaero.Controllers
{
public class DistributionController : Controller
{
private MovieContext context;
public DistributionController(MovieContext mc)
{
context = mc;
}
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create(Distribution distribution)
{
if (ModelState.IsValid)
{
context.Add(distribution);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
else
return View();
}
public IActionResult Index(int id)
{
string cookieValueFromReq = Request.Cookies["sortCookie"];
List<Distribution> dList;
if (string.IsNullOrEmpty(cookieValueFromReq))
dList = GetRecords(id);
else
{
string sortColumn = cookieValueFromReq.Split(',')[0];
string sortValue = cookieValueFromReq.Split(',')[1];
dList = GetRecords(id, sortColumn, sortValue);
}
return View(dList);
}
List<Distribution> GetRecords(int page, string sortColumn = "", string sortValue = "")
{
int pageSize = 1;
PagingInfo pagingInfo = new PagingInfo();
pagingInfo.CurrentPage = page == 0 ? 1 : page;
pagingInfo.TotalItems = context.Distribution.Count();
pagingInfo.ItemsPerPage = pageSize;
var skip = pageSize * (Convert.ToInt32(page) - 1);
ViewBag.PagingInfo = pagingInfo;
List<Distribution> result;
if (sortColumn == "")
result = context.Distribution.Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
else
{
if (sortValue == "asc")
{
switch (sortColumn)
{
case "Id":
result = context.Distribution.OrderBy(s => s.Id).Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
break;
case "Name":
result = context.Distribution.OrderBy(s => s.Name).Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
break;
case "Location":
result = context.Distribution.OrderBy(s => s.Location).Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
break;
default:
result = context.Distribution.OrderBy(s => s.Name).Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
break;
}
}
else
{
switch (sortColumn)
{
case "Id":
result = context.Distribution.OrderByDescending(s => s.Id).Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
break;
case "Name":
result = context.Distribution.OrderByDescending(s => s.Name).Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
break;
case "Location":
result = context.Distribution.OrderByDescending(s => s.Location).Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
break;
default:
result = context.Distribution.OrderByDescending(s => s.Name).Skip(skip).Take(pageSize).Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R).ToList();
break;
}
}
}
return result;
}
}
}
Points to note:
Include(s => s.MovieDistribution_R).ThenInclude(r => r.Movie_R).ThenInclude(t => t.MovieDetail_R)
We will also show these related records on the Index view. So add the Index View inside the Views ➤ Distribution folder and having the below code:
@model List<Distribution>
@{
ViewData["Title"] = "Distribution";
}
<h1 class="bg-info text-white">Movies</h1>
<a asp-action="Create" class="btn btn-secondary">Create Distribution</a>
<table class="table table-sm table-bordered">
<tr>
<th class="sort">Id</th>
<th class="sort">Name</th>
<th>Movie</th>
<th class="sort">Location</th>
<th>Telephone</th>
<th>Update</th>
<th>Delete</th>
</tr>
@foreach (Distribution distribution in Model)
{
<tr>
<td>@distribution.Id</td>
<td>@distribution.Name</td>
<td>
<table>
<tr>
@foreach (MovieDistribution md in distribution.MovieDistribution_R)
{
<td>
@md.Movie_R.MovieDetail_R.Name
</td>
}
</tr>
</table>
</td>
<td>@distribution.Location</td>
<td>@distribution.Telephone</td>
<td>
<a class="btn btn-sm btn-primary" asp-action="Update" asp-route-id="@distribution.Id">
Update
</a>
</td>
<td>
<form asp-action="Delete" asp-route-id="@distribution.Id" method="post">
<button type="submit" class="btn btn-sm btn-danger">
Delete
</button>
</form>
</td>
</tr>
}
</table>
<div class="pagingDiv" page-model="@ViewBag.PagingInfo" page-action="Index" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div>
Here notice the inner foreach loop whose task is to show the related MovieDetail information of the distribution records inside the HTML Table.
@foreach (MovieDistribution md in distribution.MovieDistribution_R)
{
<td>
@md.Movie_R.MovieDetail_R.Name
</td>
}
First add a few Distribution records, see the below given video where we are adding a Distribution record.
Next, video shows all the Distribution records in page by page way. The sorting of records is also shown.
Notice the Movie column in the distribution table is empty (see above video). Next, we will show the Many-to-Many Relationship being created between the Movies & Distributions, and this will make the Movie column to start showing associated movies with a distribution.
Next we did some movie associations for the Beta & Gamma distributions to show Many-to-Many Relationship. See the below image which shows these 2 thing:
This is because of Many-to-Many Relationship between the Movie & Distribution tables of the database.
The Update action will perform the updation task of the Distribution records. It’s code is fairly simple and shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
using Xaero.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Xaero.Controllers
{
public class DistributionController : Controller
{
private MovieContext context;
public DistributionController(MovieContext mc)
{
context = mc;
}
//… other actions
public IActionResult Update(int id)
{
return View(context.Distribution.Where(a => a.Id == id).FirstOrDefault());
}
[HttpPost]
public async Task<IActionResult> Update(Distribution distribution)
{
if (ModelState.IsValid)
{
context.Update(distribution);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
else
return View();
}
}
}
Also add the Update View inside the Views ➤ Distribution folder and having the below code:
@model Distribution
@{
ViewData["Title"] = "Update a Distribution";
}
<h1 class="bg-info text-white">Update a Distribution</h1>
<a asp-action="Index" class="btn btn-secondary">View all Distribution</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post">
<div class="form-group">
<label asp-for="Id"></label>
<input type="text" asp-for="Id" disabled class="form-control" />
</div>
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Location">Location</label>
<input type="text" asp-for="Location" class="form-control" />
<span asp-validation-for="Location" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Telephone"></label>
<input type="text" asp-for="Telephone" class="form-control" />
<span asp-validation-for="Telephone" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
Check the below video for it’s working.
Add the Delete action whose work is to delete the Distribution records from the database. See below code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Xaero.Models;
using Xaero.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Xaero.Controllers
{
public class DistributionController : Controller
{
private MovieContext context;
public DistributionController(MovieContext mc)
{
context = mc;
}
public IActionResult Create()
{
return View();
}
//… other actions
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
var distribution = context.Distribution.Where(a => a.Id == id).FirstOrDefault();
context.Remove(distribution);
await context.SaveChangesAsync();
return RedirectToAction("Index");
}
}
}
Check the below video for it’s working.
Congratulations, we have just completed a very long Entity Framework Core Tutorial of 10,000+ words where we learned all the things to make a professional database project. I hope you liked this tutorial and all the concepts which we have explained. So please share it on your facebook, twitter and linked for necessary exposure with the coding community.