Prerequisites: View or Download Sample Code How To Download
Prerequisites: View or Download Sample Code How To Download
Prerequisites: View or Download Sample Code How To Download
At the end of the series, you'll have an app that manages and displays movie data.
You learn how to:
Prerequisites
• Visual Studio
• Visual Studio Code
• Visual Studio for Mac
• Visual Studio 2019 16.8 or later with the ASP.NET and web
development workload
• .NET 5.0 SDK
• Visual Studio
• Visual Studio Code
• Visual Studio for Mac
• Start Visual Studio and select Create a new project.
• In the Create a new project dialog, select ASP.NET Core Web
Application > Next.
• In the Configure your new project dialog, enter MvcMovie for Project
name. It's important to name the project MvcMovie. Capitalization needs
to match each namespace matches when code is copied.
• Select Create.
• In the Create a new ASP.NET Core web application dialog, select:
o .NET Core and ASP.NET Core 5.0 in the dropdowns.
o ASP.NET Core Web App (Model-View-Controller).
o Create.
For alternative approaches to create the project, see Create a new project in Visual
Studio.
Visual Studio used the default project template for the created MVC project. The
created project:
• Is a working app.
• Is a basic starter project.
Visual Studio displays the following dialog when a project is not yet
configured to use SSL:
Visual Studio:
Launching the app without debugging by selecting Ctrl+F5 allows you to:
You can launch the app in debug or non-debug mode from the Debug menu item:
You can debug the app by selecting the IIS Express button
• Models: Classes that represent the data of the app. The model classes
use validation logic to enforce business rules for that data. Typically,
model objects retrieve and store model state in a database. In this
tutorial, a Movie model retrieves movie data from a database, provides it
to the view or updates it. Updated data is written to a database.
• Views: Views are the components that display the app's user interface
(UI). Generally, this UI displays the model data.
• Controllers: Classes that:
o Handle browser requests.
o Retrieve model data.
o Call view templates that return a response.
In an MVC app, the view only displays information. The controller handles and
responds to user input and interaction. For example, the controller handles URL
segments and query-string values, and passes these values to the model. The model
might use these values to query the database. For example:
The MVC architectural pattern separates an app into three main groups of
components: Models, Views, and Controllers. This pattern helps to achieve separation
of concerns: The UI logic belongs in the view. Input logic belongs in the controller.
Business logic belongs in the model. This separation helps manage complexity when
building an app, because it enables work on one aspect of the implementation at a
time without impacting the code of another. For example, you can work on the view
code without depending on the business logic code.
These concepts are introduced and demonstrated in this tutorial series while building
a movie app. The MVC project contains folders for the Controllers and Views.
Add a controller
• Visual Studio
• Visual Studio Code
• Visual Studio for Mac
In the Solution Explorer, right-click Controllers > Add > Controller.
In the Add Scaffold dialog box, select MVC Controller - Empty.
C#Copy
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/
//
// GET: /HelloWorld/Welcome/
An HTTP endpoint:
The first comment states this is an HTTP GET method that's invoked by
appending /HelloWorld/ to the base URL.
Append "HelloWorld" to the path in the address bar. The Index method returns a
string.
MVC invokes controller classes, and the action methods within them, depending on
the incoming URL. The default URL routing logic used by MVC, uses a format like this
to determine what code to invoke:
/[Controller]/[ActionName]/[Parameters]
C#Copy
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
When you browse to the app and don't supply any URL segments, it defaults to the
"Home" controller and the "Index" method specified in the template line highlighted
above. In the preceding URL segments:
Modify the code to pass some parameter information from the URL to the controller.
For example, /HelloWorld/Welcome?name=Rick&numtimes=4.
Change the Welcome method to include two parameters as shown in the following
code.
C#Copy
// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
C#Copy
public string Welcome(string name, int ID = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}
In this section, you modify the HelloWorldController class to use Razor view files. This
cleanly encapsulates the process of generating HTML responses to a client.
Currently the Index method returns a string with a message in the controller class. In
the HelloWorldController class, replace the Index method with the following code:
C#Copy
public IActionResult Index()
{
return View();
}
Controller methods:
• Are referred to as action methods. For example, the Index action method
in the preceding code.
• Generally return an IActionResult or a class derived from ActionResult,
not a type like string.
Add a view
• Visual Studio
•Visual Studio Code
• Visual Studio for Mac
Right-click on the Views folder, and then Add > New Folder and name the
folder HelloWorld.
Right-click on the Views/HelloWorld folder, and then Add > New Item.
In the Add New Item - MvcMovie dialog:
Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the
following:
CSHTMLCopy
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
Navigate to https://localhost:{PORT}/HelloWorld:
Find the @RenderBody() line. RenderBody is a placeholder where all the view-specific
pages you create show up, wrapped in the layout page. For example, if you select
the Privacy link, the Views/Home/Privacy.cshtml view is rendered inside
the RenderBody method.
CSHTMLCopy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-
white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-controller="Movies" asp-
action="Index">Movie App</a>
<button class="navbar-toggler" type="button" data-
toggle="collapse" data-target=".navbar-collapse" aria-
controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-
content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
In the preceding markup, the asp-area="" anchor Tag Helper attribute and attribute
value was omitted because this app isn't using Areas.
Note: The Movies controller hasn't been implemented. At this point, the Movie App link
isn't functional.
Save the changes and select the Privacy link. Notice how the title on the browser tab
displays Privacy Policy - Movie App instead of Privacy Policy - Mvc Movie:
Select the Home link.
Notice that the title and anchor text display Movie App. The changes were made
once in the layout template and all pages on the site reflect the new link text and
new title.
CSHTMLCopy
@{
Layout = "_Layout";
}
CSHTMLCopy
@{
ViewData["Title"] = "Movie List";
}
ViewData["Title"] = "Movie List"; in the code above sets the Title property of
the ViewData dictionary to "Movie List". The Title property is used in
the <title> HTML element in the layout page:
CSHTMLCopy
<title>@ViewData["Title"] - Movie App</title>
• Browser title.
• Primary heading.
• Secondary headings.
If there are no changes in the browser, it could be cached content that is being
viewed. Press Ctrl+F5 in the browser to force the response from the server to be
loaded. The browser title is created with ViewData["Title"] we set in
the Index.cshtml view template and the additional "- Movie App" added in the layout
file.
Controllers are responsible for providing the data required in order for a view
template to render a response.
• Do business logic
• Interact with a database directly.
A view template should work only with the data that's provided to it by the
controller. Maintaining this "separation of concerns" helps keep the code:
• Clean.
• Testable.
• Maintainable.
Currently, the Welcome method in the HelloWorldController class takes a name and
a ID parameter and then outputs the values directly to the browser.
Rather than have the controller render this response as a string, change the
controller to use a view template instead. The view template generates a dynamic
response, which means that appropriate data must be passed from the controller to
the view to generate the response. Do this by having the controller put the dynamic
data (parameters) that the view template needs in a ViewData dictionary. The view
template can then access the dynamic data.
The ViewData dictionary is a dynamic object, which means any type can be used.
The ViewData object has no defined properties until something is added. The MVC
model binding system automatically maps the named
parameters name and numTimes from the query string to parameters in the method.
The complete HelloWorldController:
C#Copy
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
public IActionResult Index()
{
return View();
}
return View();
}
}
}
The ViewData dictionary object contains data that will be passed to the view.
CSHTMLCopy
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4
Data is taken from the URL and passed to the controller using the MVC model
binder. The controller packages the data into a ViewData dictionary and passes that
object to the view. The view then renders the data as HTML to the browser.
In this tutorial, classes are added for managing movies in a database. These classes
are the "Model" part of the MVC app.
These model classes are used with Entity Framework Core (EF Core) to work with a
database. EF Core is an object-relational mapping (ORM) framework that simplifies
the data access code that you have to write.
The model classes created are known as POCO classes, from Plain Old CLR Objects.
POCO classes don't have any dependency on EF Core. They only define the
properties of the data to be stored in the database.
In this tutorial, model classes are created first, and EF Core creates the database.
• Visual Studio
• Visual Studio Code
• Visual Studio for Mac
Right-click the Models folder > Add > Class. Name the file Movie.cs.
C#Copy
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
The Movie class contains an Id field, which is required by the database for the primary
key.
The DataType attribute on ReleaseDate specifies the type of the data (Date). With this
attribute:
• The user isn't required to enter time information in the date field.
• Only the date is displayed, not time information.
• Visual Studio
• Visual Studio Code
• Visual Studio for Mac
From the Tools menu, select NuGet Package Manager > Package Manager
Console (PMC).
PowerShellCopy
Install-Package Microsoft.EntityFrameworkCore.Design
• Visual Studio
• Visual Studio Code
• Visual Studio for Mac
In Solution Explorer, right-click the Controllers folder and select Add > New
Scaffolded Item.
In the Add Scaffold dialog, select MVC Controller with views, using Entity
Framework > Add.
Complete the Add MVC Controller with views, using Entity Framework dialog:
The automatic creation of these files and file updates are known as scaffolding.
The scaffolded pages can't be used yet because the database doesn't exist. Running
the app and selecting the Movie App link results in a Cannot open database or no
such table: Movie error message.
Initial migration
Use the EF Core Migrations feature to create the database. Migrations are a set of
tools that create and update a database to match the data model.
• Visual Studio
• Visual Studio Code / Visual Studio for Mac
From the Tools menu, select NuGet Package Manager > Package Manager
Console .
PowerShellCopy
Add-Migration InitialCreate
Update-Database
No type was specified for the decimal column 'Price' on entity type 'Movie'. This will
cause values to be silently truncated if they do not fit in the default precision and
scale. Explicitly specify the SQL server column type that can accommodate all the
values using 'HasColumnType()'.
For more information on the PMC tools for EF Core, see EF Core tools reference -
PMC in Visual Studio.
• Visual Studio
• Visual Studio Code / Visual Studio for Mac
ConsoleCopy
SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The
login failed.
Note
You may not be able to enter decimal commas in the Price field. To support jQuery
validation for non-English locales that use a comma (",") for a decimal point and for
non US-English date formats, the app must be globalized. For globalization
instructions, see this GitHub issue.
With EF Core, data access is performed using a model. A model is made up of entity
classes and a context object that represents a session with the database. The context
object allows querying and saving data. The database context is derived
from Microsoft.EntityFrameworkCore.DbContext and specifies the entities to include
in the data model.
C#Copy
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;
namespace MvcMovie.Data
{
public class MvcMovieContext : DbContext
{
public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
: base(options)
{
}
The preceding code creates a DbSet<Movie> property that represents the movies in
the database.
ASP.NET Core is built with dependency injection (DI). Services, such as the database
context, must be registered with DI in Startup. Components that require these
services are provided via constructor parameters.
In the Controllers/MoviesController.cs file, the constructor uses Dependency
Injection to inject the MvcMovieContext database context into the controller. The
database context is used in each of the CRUD methods in the controller.
• Visual Studio
•Visual Studio Code
• Visual Studio for Mac
C#Copy
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
• Visual Studio
•Visual Studio Code / Visual Studio for Mac
JSONCopy
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
C#Copy
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Movie",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(type: "nvarchar(max)", nullable:
true),
ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable:
false),
Genre = table.Column<string>(type: "nvarchar(max)", nullable:
true),
Price = table.Column<decimal>(type: "decimal(18,2)", nullable:
false)
},
constraints: table =>
{
table.PrimaryKey("PK_Movie", x => x.Id);
});
}
C#Copy
public class MoviesController : Controller
{
private readonly MvcMovieContext _context;
MVC provides the ability to pass strongly typed model objects to a view. This
strongly typed approach enables compile time code checking. The scaffolding
mechanism passed a strongly typed model in the MoviesController class and views.
C#Copy
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The id parameter is defined as a nullable type (int?) in cases when the id value isn't
provided.
C#Copy
var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.Id == id);
If a movie is found, an instance of the Movie model is passed to the Details view:
C#Copy
return View(movie);
CSHTMLCopy
@model MvcMovie.Models.Movie
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>Movie</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ReleaseDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ReleaseDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Genre)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Genre)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Price)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>
The @model statement at the top of the view file specifies the type of object that the
view expects. When the movie controller was created, the following @model statement
was included:
CSHTMLCopy
@model MvcMovie.Models.Movie
This @model directive allows access to the movie that the controller passed to the
view. The Model object is strongly typed. For example, in the Details.cshtml view, the
code passes each movie field to the DisplayNameFor and DisplayFor HTML Helpers
with the strongly typed Model object. The Create and Edit methods and views also
pass a Movie model object.
Examine the Index.cshtml view and the Index method in the Movies controller. Notice
how the code creates a List object when it calls the View method. The code passes
this Movies list from the Index action method to the view:
C#Copy
// GET: Movies
public async Task<IActionResult> Index()
{
return View(await _context.Movie.ToListAsync());
}
CSHTMLCopy
@model IEnumerable<MvcMovie.Models.Movie>
The @model directive allows access to the list of movies that the controller passed to
the view by using a Model object that's strongly typed. For example, in
the Index.cshtml view, the code loops through the movies with a foreach statement
over the strongly typed Model object:
CSHTMLCopy
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Because the Model object is strongly typed as an IEnumerable<Movie> object, each item
in the loop is typed as Movie. Among other benefits, the compiler validates the types
used in the code.
JSONCopy
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB-
2;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
"AllowedHosts": "*"
}
With the preceding JSON, SQL statements are displayed on the command line and in
the Visual Studio output window.
The MvcMovieContext object handles the task of connecting to the database and
mapping Movie objects to database records. The database context is registered with
the Dependency Injection container in the ConfigureServices method in
the Startup.cs file:
• Visual Studio
•Visual Studio Code / Visual Studio for Mac
C#Copy
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
The ASP.NET Core Configuration system reads the ConnectionString key. For local
development, it gets the connection string from the appsettings.json file:
JSONCopy
"ConnectionStrings": {
"MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-
2;Trusted_Connection=True;MultipleActiveResultSets=true"
}
• Visual Studio
• Visual Studio Code / Visual Studio for Mac
From the View menu, open SQL Server Object Explorer (SSOX).
Right-click on the Movie table > View Designer
Note the key icon next to ID. By default, EF makes a property named ID the primary
key.
C#Copy
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;
namespace MvcMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<
DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}
If there are any movies in the database, the seed initializer returns and no movies are
added.
C#Copy
if (context.Movie.Any())
{
return; // DB has been seeded.
}
C#Copy
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MvcMovie.Data;
using MvcMovie.Models;
using System;
namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
try
{
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
• Visual Studio
• Visual Studio Code / Visual Studio for Mac
Delete all the records in the database. You can do this with the delete links in the
browser or from SSOX.
Force the app to initialize, calling the methods in the Startup class, so the seed
method runs. To force initialization, IIS Express must be stopped and restarted. You
can do this with any of the following approaches:
• Right-click the IIS Express system tray icon in the notification area and
tap Exit or Stop Site:
C#Copy
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
Browse to the Movies controller and hold the mouse pointer over an Edit link to see
the target URL.
The Edit, Details, and Delete links are generated by the Core MVC Anchor Tag
Helper in the Views/Movies/Index.cshtml file.
CSHTMLCopy
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
Tag Helpers enable server-side code to participate in creating and rendering HTML
elements in Razor files. In the code above, the AnchorTagHelper dynamically generates
the HTML href attribute value from the controller action method and route id. You
use View Source from your favorite browser or use the developer tools to examine
the generated markup. A portion of the generated HTML is shown below:
HTMLCopy
<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>
C#Copy
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
Tag Helpers are one of the most popular new features in ASP.NET Core. For more
information, see Additional resources.
Open the Movies controller and examine the two Edit action methods. The following
code shows the HTTP GET Edit method, which fetches the movie and populates the
edit form generated by the Edit.cshtml Razor file.
C#Copy
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted
movie values:
C#Copy
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you
want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id,
[Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The [Bind] attribute is one way to protect against over-posting. You should only
include properties in the [Bind] attribute that you want to change. For more
information, see Protect your controller from over-posting. ViewModels provide an
alternative approach to prevent over-posting.
Notice the second Edit action method is preceded by the [HttpPost] attribute.
C#Copy
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id,
[Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
CSHTMLCopy
<form asp-action="Edit">
The Form Tag Helper generates a hidden anti-forgery token that must match
the [ValidateAntiForgeryToken] generated anti-forgery token in the Edit method of
the Movies controller. For more information, see Prevent Cross-Site Request Forgery
(XSRF/CSRF) attacks in ASP.NET Core.
The HttpGet Edit method takes the movie ID parameter, looks up the movie using
the Entity Framework FindAsync method, and returns the selected movie to the Edit
view. If a movie cannot be found, NotFound (HTTP 404) is returned.
C#Copy
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
When the scaffolding system created the Edit view, it examined the Movie class and
created code to render <label> and <input> elements for each property of the class.
The following example shows the Edit view that was generated by the Visual Studio
scaffolding system:
CSHTMLCopy
@model MvcMovie.Models.Movie
@{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="control-label"></label>
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Genre" class="control-label"></label>
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Notice how the view template has a @model MvcMovie.Models.Movie statement at the
top of the file. @model MvcMovie.Models.Movie specifies that the view expects the
model for the view template to be of type Movie.
The scaffolded code uses several Tag Helper methods to streamline the HTML
markup. The Label Tag Helper displays the name of the field ("Title", "ReleaseDate",
"Genre", or "Price"). The Input Tag Helper renders an HTML <input> element.
The Validation Tag Helper displays any validation messages associated with that
property.
Run the application and navigate to the /Movies URL. Click an Edit link. In the
browser, view the source for the page. The generated HTML for the <form> element is
shown below.
HTMLCopy
<form action="/Movies/Edit/7" method="post">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div class="text-danger" />
<input type="hidden" data-val="true" data-val-required="The ID field is
required." id="ID" name="ID" value="7" />
<div class="form-group">
<label class="control-label col-md-2" for="Genre" />
<div class="col-md-10">
<input class="form-control" type="text" id="Genre" name="Genre"
value="Western" />
<span class="text-danger field-validation-valid" data-valmsg-
for="Genre" data-valmsg-replace="true"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="Price" />
<div class="col-md-10">
<input class="form-control" type="text" data-val="true" data-val-
number="The field Price must be a number." data-val-required="The Price field is
required." id="Price" name="Price" value="3.99" />
<span class="text-danger field-validation-valid" data-valmsg-
for="Price" data-valmsg-replace="true"></span>
</div>
</div>
<!-- Markup removed for brevity -->
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
<input name="__RequestVerificationToken" type="hidden"
value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJB
XhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU"
/>
</form>
The <input> elements are in an HTML <form> element whose action attribute is set to
post to the /Movies/Edit/id URL. The form data will be posted to the server when
the Save button is clicked. The last line before the closing </form> element shows the
hidden XSRF token generated by the Form Tag Helper.
C#Copy
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id,
[Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
The model binding system takes the posted form values and creates a Movie object
that's passed as the movie parameter. The ModelState.IsValid property verifies that
the data submitted in the form can be used to modify (edit or update) a Movie object.
If the data is valid, it's saved. The updated (edited) movie data is saved to the
database by calling the SaveChangesAsync method of database context. After saving
the data, the code redirects the user to the Index action method of
the MoviesController class, which displays the movie collection, including the changes
just made.
Before the form is posted to the server, client-side validation checks any validation
rules on the fields. If there are any validation errors, an error message is displayed
and the form isn't posted. If JavaScript is disabled, you won't have client-side
validation but the server will detect the posted values that are not valid, and the form
values will be redisplayed with error messages. Later in the tutorial we
examine Model Validation in more detail. The Validation Tag Helper in
the Views/Movies/Edit.cshtml view template takes care of displaying appropriate error
messages.
All the HttpGet methods in the movie controller follow a similar pattern. They get a
movie object (or list of objects, in the case of Index), and pass the object (model) to
the view. The Create method passes an empty movie object to the Create view. All the
methods that create, edit, delete, or otherwise modify data do so in
the [HttpPost] overload of the method. Modifying data in an HTTP GET method is a
security risk. Modifying data in an HTTP GET method also violates HTTP best practices
and the architectural REST pattern, which specifies that GET requests shouldn't
change the state of your application. In other words, performing a GET operation
should be a safe operation that has no side effects and doesn't modify your persisted
data.
In this section, you add search capability to the Index action method that lets you
search movies by genre or name.
C#Copy
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The first line of the Index action method creates a LINQ query to select the movies:
C#Copy
var movies = from m in _context.Movie
select m;
The query is only defined at this point, it has not been run against the database.
If the searchString parameter contains a string, the movies query is modified to filter
on the value of the search string:
C#Copy
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The s => s.Title.Contains() code above is a Lambda Expression. Lambdas are used
in method-based LINQ queries as arguments to standard query operator methods
such as the Where method or Contains (used in the code above). LINQ queries are
not executed when they're defined or when they're modified by calling a method
such as Where, Contains, or OrderBy. Rather, query execution is deferred. That means
that the evaluation of an expression is delayed until its realized value is actually
iterated over or the ToListAsync method is called. For more information about
deferred query execution, see Query Execution.
Note: The Contains method is run on the database, not in the c# code shown above.
The case sensitivity on the query depends on the database and the collation. On SQL
Server, Contains maps to SQL LIKE, which is case insensitive. In SQLite, with the
default collation, it's case sensitive.
If you change the signature of the Index method to have a parameter named id,
the id parameter will match the optional {id} placeholder for the default routes set
in Startup.cs.
C#Copy
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
C#Copy
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
C#Copy
public async Task<IActionResult> Index(string id)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}
You can now pass the search title as route data (a URL segment) instead of as a
query string value.
However, you can't expect users to modify the URL every time they want to search
for a movie. So now you'll add UI elements to help them filter movies. If you changed
the signature of the Index method to test how to pass the route-bound ID parameter,
change it back so that it takes a parameter named searchString:
C#Copy
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
Open the Views/Movies/Index.cshtml file, and add the <form> markup highlighted
below:
CSHTMLCopy
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
The HTML <form> tag uses the Form Tag Helper, so when you submit the form, the
filter string is posted to the Index action of the movies controller. Save your changes
and then test the filter.
There's no [HttpPost] overload of the Index method as you might expect. You don't
need it, because the method isn't changing the state of the app, just filtering data.
The notUsed parameter is used to create an overload for the Index method. We'll talk
about that later in the tutorial.
If you add this method, the action invoker would match the [HttpPost]
Index method, and the [HttpPost] Index method would run as shown in the image
below.
However, even if you add this [HttpPost] version of the Index method, there's a
limitation in how this has all been implemented. Imagine that you want to bookmark
a particular search or you want to send a link to friends that they can click in order to
see the same filtered list of movies. Notice that the URL for the HTTP POST request is
the same as the URL for the GET request (localhost:{PORT}/Movies/Index) -- there's
no search information in the URL. The search string information is sent to the server
as a form field value. You can verify that with the browser Developer tools or the
excellent Fiddler tool. The image below shows the Chrome browser Developer tools:
You can see the search parameter and XSRF token in the request body. Note, as
mentioned in the previous tutorial, the Form Tag Helper generates an XSRF anti-
forgery token. We're not modifying data, so we don't need to validate the token in
the controller method.
Because the search parameter is in the request body and not the URL, you can't
capture that search information to bookmark or share with others. Fix this by
specifying the request should be HTTP GET found in
the Views/Movies/Index.cshtml file.
CSHTMLCopy
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
Title: <input type="text" name="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
Now when you submit a search, the URL contains the search query string. Searching
will also go to the HttpGet Index action method, even if you have a HttpPost
Index method.
The following markup shows the change to the form tag:
CSHTMLCopy
<form asp-controller="Movies" asp-action="Index" method="get">
C#Copy
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> Movies { get; set; }
public SelectList Genres { get; set; }
public string MovieGenre { get; set; }
public string SearchString { get; set; }
}
}
The movie-genre view model will contain:
• A list of movies.
• A SelectList containing the list of genres. This allows the user to select a
genre from the list.
• MovieGenre, which contains the selected genre.
• SearchString, which contains the text users enter in the search text box.
C#Copy
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
return View(movieGenreVM);
}
The following code is a LINQ query that retrieves all the genres from the database.
C#Copy
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
The SelectList of genres is created by projecting the distinct genres (we don't want
our select list to have duplicate genres).
When the user searches for the item, the search value is retained in the search box.
Add search by genre to the Index view
Update Index.cshtml found in Views/Movies/ as follows:
CSHTMLCopy
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
In the preceding code, the DisplayNameFor HTML Helper inspects the Title property
referenced in the lambda expression to determine the display name. Since the
lambda expression is inspected rather than evaluated, you don't receive an access
violation when model, model.Movies, or model.Movies[0] are null or empty. When the
lambda expression is evaluated (for example, @Html.DisplayFor(modelItem =>
item.Title)), the model's property values are evaluated.
namespace MvcMovie.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
• Visual Studio
• Visual Studio Code
• Visual Studio for Mac
Ctrl+Shift+B
Because you've added a new field to the Movie class, you need to update the
property binding list so this new property will be included. In MoviesController.cs,
update the [Bind] attribute for both the Create and Edit action methods to include
the Rating property:
C#Copy
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
Update the view templates in order to display, create, and edit the
new Rating property in the browser view.
CSHTMLCopy
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Rating)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Rating)
</td>
<td>
Update the SeedData class so that it provides a value for the new column. A sample
change is shown below, but you'll want to make this change for each new Movie.
C#Copy
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Rating = "R",
Price = 7.99M
},
The app won't work until the DB is updated to include the new field. If it's run now,
the following SqlException is thrown:
This error occurs because the updated Movie model class is different than the
schema of the Movie table of the existing database. (There's no Rating column in the
database table.)
• Visual Studio
• Visual Studio Code / Visual Studio for Mac
From the Tools menu, select NuGet Package Manager > Package Manager
Console.
PowerShellCopy
Add-Migration Rating
Update-Database
The name "Rating" is arbitrary and is used to name the migration file. It's helpful to
use a meaningful name for the migration file.
If all the records in the DB are deleted, the initialize method will seed the DB and
include the Rating field.
Run the app and verify you can create, edit, and display movies with a Rating field.