Core With Signal R
Core With Signal R
Web app For new development Get started with Razor Pages
Web app For maintaining an MVC app Get started with MVC
SCENARIO TUTORIAL
3. Read an overview of ASP.NET Core features that apply to all app types:
Fundamentals
4. Browse the Table of Contents for other topics of interest.
* There is a new web API tutorial that you follow entirely in the browser, no local IDE installation required. The
code runs in an Azure Cloud Shell, and curl is used for testing.
To change the sample to run the ExpandDefault scenario, define the ExpandDefault symbol and leave the
remaining symbols commented-out:
For more information on using C# preprocessor directives to selectively compile sections of code, see #define (C#
Reference) and #if (C# Reference).
Regions in sample code
Some sample apps contain sections of code surrounded by #region and #endregion C# statements. The
documentation build system injects these regions into the rendered documentation topics.
Region names usually contain the word "snippet." The following example shows a region named
snippet_FilterInCode :
#region snippet_FilterInCode
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
logging.AddFilter("System", LogLevel.Debug)
.AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Trace))
.Build();
#endregion
The preceding C# code snippet is referenced in the topic's markdown file with the following line:
[!code-csharp[](sample/SampleApp/Program.cs?name=snippet_FilterInCode)]
You may safely ignore (or remove) the #region and #endregion statements that surround the code. Don't alter
the code within these statements if you plan to run the sample scenarios described in the topic. Feel free to alter
the code when experimenting with other scenarios.
For more information, see Contribute to the ASP.NET documentation: Code snippets.
Next steps
For more information, see the following resources:
Get started with ASP.NET Core
Publish an ASP.NET Core app to Azure with Visual Studio
ASP.NET Core fundamentals
The weekly ASP.NET community standup covers the team's progress and plans. It features new blogs and
third-party software.
Choose between ASP.NET 4.x and ASP.NET Core
7/16/2019 • 2 minutes to read • Edit Online
ASP.NET Core is a redesign of ASP.NET 4.x. This article lists the differences between them.
ASP.NET Core
ASP.NET Core is an open-source, cross-platform framework for building modern, cloud-based web apps on
Windows, macOS, or Linux.
ASP.NET Core provides the following benefits:
A unified story for building web UI and web APIs.
Architected for testability.
Razor Pages makes coding page-focused scenarios easier and more productive.
Blazor lets you use C# in the browser alongside JavaScript. Share server-side and client-side app logic all
written with .NET.
Ability to develop and run on Windows, macOS, and Linux.
Open-source and community-focused.
Integration of modern, client-side frameworks and development workflows.
A cloud-ready, environment-based configuration system.
Built-in dependency injection.
A lightweight, high-performance, and modular HTTP request pipeline.
Ability to host on IIS, Nginx, Apache, Docker, or self-host in your own process.
Side-by-side app versioning when targeting .NET Core.
Tooling that simplifies modern web development.
ASP.NET 4.x
ASP.NET 4.x is a mature framework that provides the services needed to build enterprise-grade, server-based web
apps on Windows.
Framework selection
The following table compares ASP.NET Core to ASP.NET 4.x.
Razor Pages is the recommended approach to create a Web UI Use Web Forms, SignalR, MVC, Web API, WebHooks, or Web
as of ASP.NET Core 2.x. See also MVC, Web API, and SignalR. Pages
Develop with Visual Studio, Visual Studio for Mac, or Visual Develop with Visual Studio using C#, VB, or F#
Studio Code using C# or F#
See ASP.NET Core targeting .NET Framework for information on ASP.NET Core 2.x support on .NET Framework.
Additional resources
Introduction to ASP.NET
Introduction to ASP.NET Core
Deploy ASP.NET Core apps to Azure App Service
Tutorial: Get started with ASP.NET Core
7/11/2019 • 2 minutes to read • Edit Online
This tutorial shows how to use the .NET Core command-line interface to create and run an ASP.NET Core web
app.
You'll learn how to:
Create a web app project.
Trust the development certificate.
Run the app.
Edit a Razor page.
At the end, you'll have a working web app running on your local machine.
Prerequisites
.NET Core 2.2 SDK
cd aspnetcoreapp
dotnet run
After the command shell indicates that the app has started, browse to https://localhost:5001. Click Accept to
accept the privacy and cookie policy. This app doesn't keep personal information.
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Hello, world! The time on the server is @DateTime.Now</p>
</div>
Next steps
In this tutorial, you learned how to:
Create a web app project.
Trust the development certificate.
Run the project.
Make a change.
To learn more about ASP.NET Core, see the recommended learning path in the introduction:
Introduction to ASP.NET Core
What's new in ASP.NET Core 2.2
6/21/2019 • 5 minutes to read • Edit Online
This article highlights the most significant changes in ASP.NET Core 2.2, with links to relevant documentation.
Endpoint Routing
ASP.NET Core 2.2 uses a new endpoint routing system for improved dispatching of requests. The changes include
new link generation API members and route parameter transformers.
For more information, see the following resources:
Endpoint routing in 2.2
Route parameter transformers (see Routing section)
Differences between IRouter- and endpoint-based routing
Health checks
A new health checks service makes it easier to use ASP.NET Core in environments that require health checks, such
as Kubernetes. Health checks includes middleware and a set of libraries that define an IHealthCheck abstraction
and service.
Health checks are used by a container orchestrator or load balancer to quickly determine if a system is responding
to requests normally. A container orchestrator might respond to a failing health check by halting a rolling
deployment or restarting a container. A load balancer might respond to a health check by routing traffic away from
the failing instance of the service.
Health checks are exposed by an application as an HTTP endpoint used by monitoring systems. Health checks can
be configured for a variety of real-time monitoring scenarios and monitoring systems. The health checks service
integrates with the BeatPulse project. which makes it easier to add checks for dozens of popular systems and
dependencies.
For more information, see Health checks in ASP.NET Core.
HTTP/2 in Kestrel
ASP.NET Core 2.2 adds support for HTTP/2.
HTTP/2 is a major revision of the HTTP protocol. Some of the notable features of HTTP/2 are support for header
compression and fully multiplexed streams over a single connection. While HTTP/2 preserves HTTP’s semantics
(HTTP headers, methods, etc) it's a breaking change from HTTP/1.x on how this data is framed and sent over the
wire.
As a consequence of this change in framing, servers and clients need to negotiate the protocol version used.
Application-Layer Protocol Negotiation (ALPN ) is a TLS extension that allows the server and client to negotiate the
protocol version used as part of their TLS handshake. While it is possible to have prior knowledge between the
server and the client on the protocol, all major browsers support ALPN as the only way to establish an HTTP/2
connection.
For more information, see HTTP/2 support.
Kestrel configuration
In earlier versions of ASP.NET Core, Kestrel options are configured by calling UseKestrel . In 2.2, Kestrel options
are configured by calling ConfigureKestrel on the host builder. This change resolves an issue with the order of
IServer registrations for in-process hosting. For more information, see the following resources:
CORS improvements
In earlier versions of ASP.NET Core, CORS Middleware allows Accept , Accept-Language , Content-Language , and
Origin headers to be sent regardless of the values configured in CorsPolicy.Headers . In 2.2, a CORS Middleware
policy match is only possible when the headers sent in Access-Control-Request-Headers exactly match the headers
stated in WithHeaders .
For more information, see CORS Middleware.
Response compression
ASP.NET Core 2.2 can compress responses with the Brotli compression format.
For more information, see Response Compression Middleware supports Brotli compression.
Project templates
ASP.NET Core web project templates were updated to Bootstrap 4 and Angular 6. The new look is visually simpler
and makes it easier to see the important structures of the app.
Validation performance
MVC’s validation system is designed to be extensible and flexible, allowing you to determine on a per request basis
which validators apply to a given model. This is great for authoring complex validation providers. However, in the
most common case an application only uses the built-in validators and don’t require this extra flexibility. Built-in
validators include DataAnnotations such as [Required] and [StringLength], and IValidatableObject .
In ASP.NET Core 2.2, MVC can short-circuit validation if it determines that a given model graph doesn't require
validation. Skipping validation results in significant improvements when validating models that can't or don't have
any validators. This includes objects such as collections of primitives (such as byte[] , string[] ,
Dictionary<string, string> ), or complex object graphs without many validators.
Additional information
For the complete list of changes, see the ASP.NET Core 2.2 Release Notes.
What's new in ASP.NET Core 2.1
5/6/2019 • 6 minutes to read • Edit Online
This article highlights the most significant changes in ASP.NET Core 2.1, with links to relevant documentation.
SignalR
SignalR has been rewritten for ASP.NET Core 2.1. ASP.NET Core SignalR includes a number of improvements:
A simplified scale-out model.
A new JavaScript client with no jQuery dependency.
A new compact binary protocol based on MessagePack.
Support for custom protocols.
A new streaming response model.
Support for clients based on bare WebSockets.
For more information, see ASP.NET Core SignalR.
HTTPS
With the increased focus on security and privacy, enabling HTTPS for web apps is important. HTTPS enforcement
is becoming increasingly strict on the web. Sites that don’t use HTTPS are considered insecure. Browsers
(Chromium, Mozilla) are starting to enforce that web features must be used from a secure context. GDPR requires
the use of HTTPS to protect user privacy. While using HTTPS in production is critical, using HTTPS in development
can help prevent issues in deployment (for example, insecure links). ASP.NET Core 2.1 includes a number of
improvements that make it easier to use HTTPS in development and to configure HTTPS in production. For more
information, see Enforce HTTPS.
On by default
To facilitate secure website development, HTTPS is now enabled by default. Starting in 2.1, Kestrel listens on
https://localhost:5001 when a local development certificate is present. A development certificate is created:
As part of the .NET Core SDK first-run experience, when you use the SDK for the first time.
Manually using the new dev-certs tool.
Run dotnet dev-certs https --trust to trust the certificate.
HTTPS redirection and enforcement
Web apps typically need to listen on both HTTP and HTTPS, but then redirect all HTTP traffic to HTTPS. In 2.1,
specialized HTTPS redirection middleware that intelligently redirects based on the presence of configuration or
bound server ports has been introduced.
Use of HTTPS can be further enforced using HTTP Strict Transport Security Protocol (HSTS ). HSTS instructs
browsers to always access the site via HTTPS. ASP.NET Core 2.1 adds HSTS middleware that supports options for
max age, subdomains, and the HSTS preload list.
Configuration for production
In production, HTTPS must be explicitly configured. In 2.1, default configuration schema for configuring HTTPS for
Kestrel has been added. Apps can be configured to use:
Multiple endpoints including the URLs. For more information, see Kestrel web server implementation: Endpoint
configuration.
The certificate to use for HTTPS either from a file on disk or from a certificate store.
GDPR
ASP.NET Core provides APIs and templates to help meet some of the EU General Data Protection Regulation
(GDPR ) requirements. For more information, see GDPR support in ASP.NET Core. A sample app shows how to
use and lets you test most of the GDPR extension points and APIs added to the ASP.NET Core 2.1 templates.
Integration tests
A new package is introduced that streamlines test creation and execution. The Microsoft.AspNetCore.Mvc.Testing
package handles the following tasks:
Copies the dependency file (*.deps) from the tested app into the test project's bin folder.
Sets the content root to the tested app's project root so that static files and pages/views are found when the
tests are executed.
Provides the WebApplicationFactory class to streamline bootstrapping the tested app with TestServer.
The following test uses xUnit to check that the Index page loads with a success status code and with the correct
Content-Type header:
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly HttpClient _client;
[Fact]
public async Task GetHomePage()
{
// Act
var response = await _client.GetAsync("/");
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
[ApiController], ActionResult<T>
ASP.NET Core 2.1 adds new programming conventions that make it easier to build clean and descriptive web
APIs. ActionResult<T> is a new type added to allow an app to return either a response type or any other action
result (similar to IActionResult), while still indicating the response type. The [ApiController] attribute has also
been added as the way to opt in to Web API-specific conventions and behaviors.
For more information, see Build Web APIs with ASP.NET Core.
IHttpClientFactory
ASP.NET Core 2.1 includes a new IHttpClientFactory service that makes it easier to configure and consume
instances of HttpClient in apps. HttpClient already has the concept of delegating handlers that could be linked
together for outgoing HTTP requests. The factory:
Makes registering of instances of HttpClient per named client more intuitive.
Implements a Polly handler that allows Polly policies to be used for Retry, CircuitBreakers, etc.
For more information, see Initiate HTTP Requests.
Additional information
For the complete list of changes, see the ASP.NET Core 2.1 Release Notes.
What's new in ASP.NET Core 2.0
6/13/2019 • 5 minutes to read • Edit Online
This article highlights the most significant changes in ASP.NET Core 2.0, with links to relevant documentation.
Razor Pages
Razor Pages is a new feature of ASP.NET Core MVC that makes coding page-focused scenarios easier and more
productive.
For more information, see the introduction and tutorial:
Introduction to Razor Pages
Get started with Razor Pages
Runtime Store
Applications that use the Microsoft.AspNetCore.All metapackage automatically take advantage of the new .NET
Core Runtime Store. The Store contains all the runtime assets needed to run ASP.NET Core 2.0 applications. When
you use the Microsoft.AspNetCore.All metapackage, no assets from the referenced ASP.NET Core NuGet
packages are deployed with the application because they already reside on the target system. The assets in the
Runtime Store are also precompiled to improve application startup time.
For more information, see Runtime store
Configuration update
An IConfiguration instance is added to the services container by default in ASP.NET Core 2.0. IConfiguration in
the services container makes it easier for applications to retrieve configuration values from the container.
For information about the status of planned documentation, see the GitHub issue.
Logging update
In ASP.NET Core 2.0, logging is incorporated into the dependency injection (DI) system by default. You add
providers and configure filtering in the Program.cs file instead of in the Startup.cs file. And the default
ILoggerFactory supports filtering in a way that lets you use one flexible approach for both cross-provider filtering
and specific-provider filtering.
For more information, see Introduction to Logging.
Authentication update
A new authentication model makes it easier to configure authentication for an application using DI.
New templates are available for configuring authentication for web apps and web APIs using Azure AD B2C.
For information about the status of planned documentation, see the GitHub issue.
Identity update
We've made it easier to build secure web APIs using Identity in ASP.NET Core 2.0. You can acquire access tokens
for accessing your web APIs using the Microsoft Authentication Library (MSAL ).
For more information on authentication changes in 2.0, see the following resources:
Account confirmation and password recovery in ASP.NET Core
Enable QR Code generation for authenticator apps in ASP.NET Core
Migrate Authentication and Identity to ASP.NET Core 2.0
SPA templates
Single Page Application (SPA) project templates for Angular, Aurelia, Knockout.js, React.js, and React.js with Redux
are available. The Angular template has been updated to Angular 4. The Angular and React templates are available
by default; for information about how to get the other templates, see Create a new SPA project. For information
about how to build a SPA in ASP.NET Core, see Use JavaScript Services to Create Single Page Applications in
ASP.NET Core.
Kestrel improvements
The Kestrel web server has new features that make it more suitable as an Internet-facing server. A number of
server constraint configuration options are added in the KestrelServerOptions class's new Limits property. Add
limits for the following:
Maximum client connections
Maximum request body size
Minimum request body data rate
For more information, see Kestrel web server implementation in ASP.NET Core.
For more information, see HTTP.sys web server implementation in ASP.NET Core.
The file returned to your visitors will be decorated with the appropriate HTTP headers for the ETag and
LastModified values.
If an application visitor requests content with a Range Request header, ASP.NET Core recognizes the request and
handles the header. If the requested content can be partially delivered, ASP.NET Core appropriately skips and
returns just the requested set of bytes. You don't need to write any special handlers into your methods to adapt or
handle this feature; it's automatically handled for you.
Automatic precompilation
Razor view pre-compilation is enabled during publish by default, reducing the publish output size and application
startup time.
For more information, see Razor view compilation and precompilation in ASP.NET Core.
<LangVersion>latest</LangVersion>
For information about the status of C# 7.1 features, see the Roslyn GitHub repository.
Other documentation updates for 2.0
Visual Studio publish profiles for ASP.NET Core app deployment
Key Management
Configure Facebook authentication
Configure Twitter authentication
Configure Google authentication
Configure Microsoft Account authentication
Migration guidance
For guidance on how to migrate ASP.NET Core 1.x applications to ASP.NET Core 2.0, see the following resources:
Migrate from ASP.NET Core 1.x to ASP.NET Core 2.0
Migrate Authentication and Identity to ASP.NET Core 2.0
Additional Information
For the complete list of changes, see the ASP.NET Core 2.0 Release Notes.
To connect with the ASP.NET Core development team's progress and plans, tune in to the ASP.NET Community
Standup.
What's new in ASP.NET Core 1.1
6/12/2019 • 2 minutes to read • Edit Online
Additional Information
ASP.NET Core 1.1.0 Release Notes
To connect with the ASP.NET Core development team's progress and plans, tune in to the ASP.NET
Community Standup.
Tutorial: Create a Razor Pages web app with ASP.NET
Core
8/13/2019 • 2 minutes to read • Edit Online
This series of tutorials explains the basics of building a Razor Pages web app.
For a more advanced introduction aimed at experienced developers, see Introduction to Razor Pages.
This series includes the following tutorials:
1. Create a Razor Pages web app
2. Add a model to a Razor Pages app
3. Scaffold (generate) Razor pages
4. Work with a database
5. Update Razor pages
6. Add search
7. Add a new field
8. Add validation
At the end, you'll have an app that can display and manage a database of movies.
Additional resources
Youtube version of this tutorial
Tutorial: Get started with Razor Pages in ASP.NET
Core
7/31/2019 • 10 minutes to read • Edit Online
By Rick Anderson
This is the first tutorial of a series that teaches the basics of building an ASP.NET Core Razor Pages web app.
For a more advanced introduction aimed at experienced developers, see Introduction to Razor Pages.
At the end of the series, you'll have an app that manages a database of movies.
View or download sample code (how to download).
View or download sample code (how to download).
In this tutorial, you:
Create a Razor Pages web app.
Run the app.
Examine the project files.
At the end of this tutorial, you'll have a working Razor Pages web app that you'll build on in later tutorials.
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 3.0 Preview
Name the project RazorPagesMovie. It's important to name the project RazorPagesMovie so the
namespaces will match when you copy and paste code.
Select ASP.NET Core 3.0 in the dropdown, Web Application, and then select Create.
Next steps
Advance to the next tutorial in the series:
ADD A
M ODEL
This is the first tutorial of a series. The series teaches the basics of building an ASP.NET Core Razor Pages web app.
For a more advanced introduction aimed at experienced developers, see Introduction to Razor Pages.
At the end of the series, you'll have an app that manages a database of movies.
View or download sample code (how to download).
View or download sample code (how to download).
In this tutorial, you:
Create a Razor Pages web app.
Run the app.
Examine the project files.
At the end of this tutorial, you'll have a working Razor Pages web app that you'll build on in later tutorials.
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later
WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work with
Visual Studio.
Name the project RazorPagesMovie. It's important to name the project RazorPagesMovie so the
namespaces will match when you copy and paste code.
Select ASP.NET Core 2.2 in the dropdown, Web Application, and then select Create.
The following image shows the app after you give consent to tracking:
Examine the project files
Here's an overview of the main project folders and files that you'll work with in later tutorials.
Pages folder
Contains Razor pages and supporting files. Each Razor page is a pair of files:
A .cshtml file that contains HTML markup with C# code using Razor syntax.
A .cshtml.cs file that contains C# code that handles page events.
Supporting files have names that begin with an underscore. For example, the _Layout.cshtml file configures UI
elements common to all pages. This file sets up the navigation menu at the top of the page and the copyright notice
at the bottom of the page. For more information, see Layout in ASP.NET Core.
wwwroot folder
Contains static files, such as HTML files, JavaScript files, and CSS files. For more information, see Static files in
ASP.NET Core.
appSettings.json
Contains configuration data, such as connection strings. For more information, see Configuration in ASP.NET Core.
Program.cs
Contains the entry point for the program. For more information, see .NET Generic Host.
Startup.cs
Contains code that configures app behavior, such as whether it requires consent for cookies. For more information,
see App startup in ASP.NET Core.
Additional resources
Youtube version of this tutorial
Next steps
Advance to the next tutorial in the series:
ADD A
M ODEL
Add a model to a Razor Pages app in ASP.NET Core
8/7/2019 • 25 minutes to read • Edit Online
By Rick Anderson
In this section, classes are added for managing movies in a database. These 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
data access.
The model classes are known as POCO classes (from "plain-old CLR objects") because they don't have any
dependency on EF Core. They define the properties of the data that are stored in the database.
View or download sample code (how to download).
View or download sample code (how to download).
using System;
using System.ComponentModel.DataAnnotations;
namespace RazorPagesMovie.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; }
}
}
In the Add Scaffold dialog, select Razor Pages using Entity Framework (CRUD ) > Add.
Complete the Add Razor Pages using Entity Framework (CRUD ) dialog:
In the Model class drop down, select Movie (RazorPagesMovie.Models).
In the Data context class row, select the + (plus) sign and change the generated name from
RazorPagesMovie.Models.RazorPagesMovieContext to RazorPagesMovie.Data.RazorPagesMovieContext. This
change is not required. It creates the database context class with the correct namespace.
Select Add.
The appsettings.json file is updated with the connection string used to connect to a local database.
Files created
Visual Studio
Visual Studio Code / Visual Studio for Mac
The scaffold process creates and updates the following files:
Pages/Movies: Create, Delete, Details, Edit, and Index.
Data/RazorPagesMovieContext.cs
Updated
Startup.cs
The created and updated files are explained in the next section.
Initial migration
Visual Studio
Visual Studio Code
Visual Studio for Mac
In this section, the Package Manager Console (PMC ) is used to:
Add an initial migration.
Update the database with the initial migration.
From the Tools menu, select NuGet Package Manager > Package Manager Console.
Add-Migration InitialCreate
Update-Database
The preceding commands generate the following warning: "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()'."
You can ignore that warning, it will be fixed in a later tutorial.
The ef migrations add InitialCreate command generates code to create the initial database schema. The schema
is based on the model specified in the DbContext (In the RazorPagesMovieContext.cs file). The InitialCreate
argument is used to name the migrations. Any name can be used, but by convention a name is selected that
describes the migration.
The ef database update command runs the Up method in the Migrations/<time-stamp>_InitialCreate.cs file. The
Up method creates the database.
Visual Studio
Visual Studio Code
Visual Studio for Mac
Examine the context registered with dependency injection
ASP.NET Core is built with dependency injection. Services (such as the EF Core DB context) are registered with
dependency injection during application startup. Components that require these services (such as Razor Pages) are
provided these services via constructor parameters. The constructor code that gets a DB context instance is shown
later in the tutorial.
The scaffolding tool automatically created a DB context and registered it with the dependency injection container.
Examine the Startup.ConfigureServices method. The highlighted line was added by the scaffolder:
services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
The RazorPagesMovieContext coordinates EF Core functionality (Create, Read, Update, Delete, etc.) for the Movie
model. The data context ( RazorPagesMovieContext ) is derived from Microsoft.EntityFrameworkCore.DbContext. The
data context specifies which entities are included in the data model.
using Microsoft.EntityFrameworkCore;
namespace RazorPagesMovie.Models
{
public class RazorPagesMovieContext : DbContext
{
public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
: base(options)
{
}
The preceding code creates a DbSet<Movie> property for the entity set. In Entity Framework terminology, an entity
set typically corresponds to a database table. An entity corresponds to a row in the table.
The name of the connection string is passed in to the context by calling a method on a DbContextOptions object.
For local development, the ASP.NET Core configuration system reads the connection string from the
appsettings.json file.
The Add-Migration command generates code to create the initial database schema. The schema is based on the
model specified in the RazorPagesMovieContext (In the Data/RazorPagesMovieContext.cs file). The Initial
argument is used to name the migrations. Any name can be used, but by convention a name that describes the
migration is used. For more information, see Tutorial: Using the migrations feature - ASP.NET MVC with EF Core.
The Update-Database command runs the Up method in the Migrations/{time-stamp }_InitialCreate.cs file, which
creates the database.
Test the app
Run the app and append /Movies to the URL in the browser ( http://localhost:port/movies ).
If you get the error:
SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.
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.
Additional resources
P R E V IO U S : G E T N E X T: S C A F F O L D E D R A Z O R
STA RTE D PAGES
In this section, classes are added for managing movies in a database. These 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
data access code.
The model classes are known as POCO classes (from "plain-old CLR objects") because they don't have any
dependency on EF Core. They define the properties of the data that are stored in the database.
View or download sample code (how to download).
View or download sample code (how to download).
using System;
using System.ComponentModel.DataAnnotations;
namespace RazorPagesMovie.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; }
}
}
In the Add Scaffold dialog, select Razor Pages using Entity Framework (CRUD ) > Add.
Complete the Add Razor Pages using Entity Framework (CRUD ) dialog:
In the Model class drop down, select Movie (RazorPagesMovie.Models).
In the Data context class row, select the + (plus) sign and accept the generated name
RazorPagesMovie.Models.RazorPagesMovieContext.
Select Add.
The appsettings.json file is updated with the connection string used to connect to a local database.
The scaffold process creates and updates the following files:
Files created
Pages/Movies: Create, Delete, Details, Edit, and Index.
Data/RazorPagesMovieContext.cs
File updated
Startup.cs
The created and updated files are explained in the next section.
Initial migration
Visual Studio
Visual Studio Code
Visual Studio for Mac
In this section, the Package Manager Console (PMC ) is used to:
Add an initial migration.
Update the database with the initial migration.
From the Tools menu, select NuGet Package Manager > Package Manager Console.
Add-Migration Initial
Update-Database
The preceding commands generate the following warning: "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()'."
You can ignore that warning, it will be fixed in a later tutorial.
The ef migrations add InitialCreate command generates code to create the initial database schema. The schema
is based on the model specified in the DbContext (In the RazorPagesMovieContext.cs file). The InitialCreate
argument is used to name the migrations. Any name can be used, but by convention a name is selected that
describes the migration.
The ef database update command runs the Up method in the Migrations/<time-stamp>_InitialCreate.cs file. The
Up method creates the database.
Visual Studio
Visual Studio Code
Visual Studio for Mac
Examine the context registered with dependency injection
ASP.NET Core is built with dependency injection. Services (such as the EF Core DB context) are registered with
dependency injection during application startup. Components that require these services (such as Razor Pages) are
provided these services via constructor parameters. The constructor code that gets a DB context instance is shown
later in the tutorial.
The scaffolding tool automatically created a DB context and registered it with the dependency injection container.
Examine the Startup.ConfigureServices method. The highlighted line was added by the scaffolder:
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("RazorPagesMovieContext")));
}
The RazorPagesMovieContext coordinates EF Core functionality (Create, Read, Update, Delete, etc.) for the Movie
model. The data context ( RazorPagesMovieContext ) is derived from Microsoft.EntityFrameworkCore.DbContext. The
data context specifies which entities are included in the data model.
using Microsoft.EntityFrameworkCore;
namespace RazorPagesMovie.Models
{
public class RazorPagesMovieContext : DbContext
{
public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
: base(options)
{
}
The preceding code creates a DbSet<Movie> property for the entity set. In Entity Framework terminology, an entity
set typically corresponds to a database table. An entity corresponds to a row in the table.
The name of the connection string is passed in to the context by calling a method on a DbContextOptions object.
For local development, the ASP.NET Core configuration system reads the connection string from the
appsettings.json file.
The Add-Migration command generates code to create the initial database schema. The schema is based on the
model specified in the RazorPagesMovieContext (In the Data/RazorPagesMovieContext.cs file). The Initial
argument is used to name the migrations. Any name can be used, but by convention a name that describes the
migration is used. For more information, see Tutorial: Using the migrations feature - ASP.NET MVC with EF Core.
The Update-Database command runs the Up method in the Migrations/{time-stamp }_InitialCreate.cs file, which
creates the database.
Test the app
Run the app and append /Movies to the URL in the browser ( http://localhost:port/movies ).
If you get the error:
SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.
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.
Additional resources
P R E V IO U S : G E T N E X T: S C A F F O L D E D R A Z O R
STA RTE D PAGES
Scaffolded Razor Pages in ASP.NET Core
7/22/2019 • 16 minutes to read • Edit Online
By Rick Anderson
This tutorial examines the Razor Pages created by scaffolding in the previous tutorial.
View or download sample code (how to download).
View or download sample code (how to download).
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;
Razor Pages are derived from PageModel . By convention, the PageModel -derived class is called <PageName>Model .
The constructor uses dependency injection to add the RazorPagesMovieContext to the page. All the scaffolded pages
follow this pattern. See Asynchronous code for more information on asynchronous programing with Entity
Framework.
When a request is made for the page, the OnGetAsync method returns a list of movies to the Razor Page.
OnGetAsync or OnGet is called on a Razor Page to initialize the state for the page. In this case, OnGetAsync gets a
list of movies and displays them.
When OnGetreturns void or OnGetAsync returns Task , no return method is used. When the return type is
IActionResult or Task<IActionResult> , a return statement must be provided. For example, the
Pages/Movies/Create.cshtml.cs OnPostAsync method:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<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-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Razor can transition from HTML into C# or into Razor-specific markup. When an @ symbol is followed by a Razor
reserved keyword, it transitions into Razor-specific markup, otherwise it transitions into C#.
The @page Razor directive makes the file into an MVC action, which means that it can handle requests. @page
must be the first Razor directive on a page. @page is an example of transitioning into Razor-specific markup. See
Razor syntax for more information.
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.Movie[0].Title))
The DisplayNameFor HTML Helper inspects the Title property referenced in the lambda expression to determine
the display name. The lambda expression is inspected rather than evaluated. That means there is no access violation
when model , model.Movie , or model.Movie[0] are null or empty. When the lambda expression is evaluated (for
example, with @Html.DisplayFor(modelItem => item.Title) ), the model's property values are evaluated.
The @model directive
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
The directive specifies the type of the model passed to the Razor Page. In the preceding example, the
@model
@model line makes the PageModel -derived class available to the Razor Page. The model is used in the
@Html.DisplayNameFor and @Html.DisplayFor HTML Helpers on the page.
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
The preceding highlighted markup is an example of Razor transitioning into C#. The { and } characters enclose
a block of C# code.
The PageModel base class contains a ViewData dictionary property that can be used to add data that and pass it to
a View. Objects are added to the ViewData dictionary using a key/value pattern. In the preceding sample, the
"Title" property is added to the ViewData dictionary.
The "Title" property is used in the Pages/Shared/_Layout.cshtml file. The following markup shows the first few
lines of the _Layout.cshtml file.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>
The line @*Markup removed for brevity.*@ is a Razor comment. Unlike HTML comments ( <!-- --> ), Razor
comments are not sent to the client.
Update the layout
Change the <title> element in the Pages/Shared/_Layout.cshtml file to display Movie rather than
RazorPagesMovie.
<!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</title>
The preceding anchor element is a Tag Helper. In this case, it's the Anchor Tag Helper. The
asp-page="/Movies/Index" Tag Helper attribute and value creates a link to the /Movies/Index Razor Page. The
asp-area attribute value is empty, so the area isn't used in the link. See Areas for more information.
Save your changes, and test the app by clicking on the RpMovie link. See the _Layout.cshtml file in GitHub if you
have any problems.
Test the other links (Home, RpMovie, Create, Edit, and Delete). Each page sets the title, which you can see in the
browser tab. When you bookmark a page, the title is used for the bookmark.
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 non US-English date formats, you must take steps to globalize your app. See this
GitHub issue 4076 for instructions on adding decimal comma.
@{
Layout = "_Layout";
}
The preceding markup sets the layout file to Pages/Shared/_Layout.cshtml for all Razor files under the Pages folder.
See Layout for more information.
The Create page model
Examine the Pages/Movies/Create.cshtml.cs page model:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System;
using System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
The method initializes any state needed for the page. The Create page doesn't have any state to initialize, so
OnGet
Page is returned. Later in the tutorial, an example of OnGet initializing state is shown. The Page method creates a
PageResult object that renders the Create.cshtml page.
The Movie property uses the [BindProperty] attribute to opt-in to model binding. When the Create form posts the
form values, the ASP.NET Core runtime binds the posted values to the Movie model.
The OnPostAsync method is run when the page posts form data:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be
caught on the client-side before the form is posted. An example of a model error is posting a value for the date field
that cannot be converted to a date. Client-side validation and model validation are discussed later in the tutorial.
If there are no model errors, the data is saved, and the browser is redirected to the Index page.
The Create Razor Page
Examine the Pages/Movies/Create.cshtml Razor Page file:
@page
@model RazorPagesMovie.Pages.Movies.CreateModel
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<input asp-for="Movie.Genre" class="form-control" />
<span asp-validation-for="Movie.Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio displays the following tags in a distinctive bold font used for Tag Helpers:
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
The <form method="post"> element is a Form Tag Helper. The Form Tag Helper automatically includes an
antiforgery token.
The scaffolding engine creates Razor markup for each field in the model (except the ID ) similar to the following:
The Validation Tag Helpers ( <div asp-validation-summary and <span asp-validation-for ) display validation errors.
Validation is covered in more detail later in this series.
The Label Tag Helper ( <label asp-for="Movie.Title" class="control-label"></label> ) generates the label caption
and for attribute for the Title property.
The Input Tag Helper ( <input asp-for="Movie.Title" class="form-control"> ) uses the DataAnnotations attributes
and produces HTML attributes needed for jQuery Validation on the client-side.
For more information on Tag Helpers such as <form method="post"> , see Tag Helpers in ASP.NET Core.
Additional resources
P R E V IO U S : A D D IN G A N E X T:
M ODEL D A TA B A SE
By Rick Anderson
This tutorial examines the Razor Pages created by scaffolding in the previous tutorial.
View or download sample.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
Razor Pages are derived from PageModel . By convention, the PageModel -derived class is called <PageName>Model .
The constructor uses dependency injection to add the RazorPagesMovieContext to the page. All the scaffolded pages
follow this pattern. See Asynchronous code for more information on asynchronous programing with Entity
Framework.
When a request is made for the page, the OnGetAsync method returns a list of movies to the Razor Page.
OnGetAsync or OnGet is called on a Razor Page to initialize the state for the page. In this case, OnGetAsync gets a
list of movies and displays them.
When OnGet returns void or OnGetAsync returns Task , no return method is used. When the return type is
IActionResult or Task<IActionResult> , a return statement must be provided. For example, the
Pages/Movies/Create.cshtml.cs OnPostAsync method:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<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-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Razor can transition from HTML into C# or into Razor-specific markup. When an @ symbol is followed by a Razor
reserved keyword, it transitions into Razor-specific markup, otherwise it transitions into C#.
The @page Razor directive makes the file into an MVC action, which means that it can handle requests. @page
must be the first Razor directive on a page. @page is an example of transitioning into Razor-specific markup. See
Razor syntax for more information.
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.Movie[0].Title))
The DisplayNameFor HTML Helper inspects the Title property referenced in the lambda expression to determine
the display name. The lambda expression is inspected rather than evaluated. That means there is no access violation
when model , model.Movie , or model.Movie[0] are null or empty. When the lambda expression is evaluated (for
example, with @Html.DisplayFor(modelItem => item.Title) ), the model's property values are evaluated.
The @model directive
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
The directive specifies the type of the model passed to the Razor Page. In the preceding example, the
@model
@model line makes the PageModel -derived class available to the Razor Page. The model is used in the
@Html.DisplayNameFor and @Html.DisplayFor HTML Helpers on the page.
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
The preceding highlighted code is an example of Razor transitioning into C#. The { and } characters enclose a
block of C# code.
The PageModel base class has a ViewData dictionary property that can be used to add data that you want to pass to
a View. You add objects into the ViewData dictionary using a key/value pattern. In the preceding sample, the "Title"
property is added to the ViewData dictionary.
The "Title" property is used in the Pages/Shared/_Layout.cshtml file. The following markup shows the first few lines
of the _Layout.cshtml file.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>
The line @*Markup removed for brevity.*@ is a Razor comment which doesn't appear in your layout file. Unlike
HTML comments ( <!-- --> ), Razor comments are not sent to the client.
Update the layout
Change the <title> element in the Pages/Shared/_Layout.cshtml file to display Movie rather than
RazorPagesMovie.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie</title>
The preceding anchor element is a Tag Helper. In this case, it's the Anchor Tag Helper. The
asp-page="/Movies/Index" Tag Helper attribute and value creates a link to the /Movies/Index Razor Page. The
asp-area attribute value is empty, so the area isn't used in the link. See Areas for more information.
Save your changes, and test the app by clicking on the RpMovie link. See the _Layout.cshtml file in GitHub if you
have any problems.
Test the other links (Home, RpMovie, Create, Edit, and Delete). Each page sets the title, which you can see in the
browser tab. When you bookmark a page, the title is used for the bookmark.
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 non US-English date formats, you must take steps to globalize your app. This
GitHub issue 4076 for instructions on adding decimal comma.
@{
Layout = "_Layout";
}
The preceding markup sets the layout file to Pages/Shared/_Layout.cshtml for all Razor files under the Pages folder.
See Layout for more information.
The Create page model
Examine the Pages/Movies/Create.cshtml.cs page model:
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System;
using System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
The method initializes any state needed for the page. The Create page doesn't have any state to initialize, so
OnGet
Page is returned. Later in the tutorial you see OnGet method initialize state. The Page method creates a
PageResult object that renders the Create.cshtml page.
The Movie property uses the [BindProperty] attribute to opt-in to model binding. When the Create form posts the
form values, the ASP.NET Core runtime binds the posted values to the Movie model.
The OnPostAsync method is run when the page posts form data:
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be
caught on the client-side before the form is posted. An example of a model error is posting a value for the date field
that cannot be converted to a date. Client-side validation and model validation are discussed later in the tutorial.
If there are no model errors, the data is saved, and the browser is redirected to the Index page.
The Create Razor Page
Examine the Pages/Movies/Create.cshtml Razor Page file:
@page
@model RazorPagesMovie.Pages.Movies.CreateModel
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<input asp-for="Movie.Genre" class="form-control" />
<span asp-validation-for="Movie.Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio displays the <form method="post"> tag in a distinctive bold font used for Tag Helpers:
The <form method="post"> element is a Form Tag Helper. The Form Tag Helper automatically includes an
antiforgery token.
The scaffolding engine creates Razor markup for each field in the model (except the ID ) similar to the following:
The Validation Tag Helpers ( <div asp-validation-summary and <span asp-validation-for ) display validation errors.
Validation is covered in more detail later in this series.
The Label Tag Helper ( <label asp-for="Movie.Title" class="control-label"></label> ) generates the label caption
and for attribute for the Title property.
The Input Tag Helper ( <input asp-for="Movie.Title" class="form-control"> ) uses the DataAnnotations attributes
and produces HTML attributes needed for jQuery Validation on the client-side.
Additional resources
YouTube version of this tutorial
P R E V IO U S : A D D IN G A N E X T:
M ODEL D A TA B A SE
Work with a database and ASP.NET Core
7/24/2019 • 12 minutes to read • Edit Online
Visual Studio
Visual Studio Code / Visual Studio for Mac
services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
The ASP.NET Core Configuration system reads the ConnectionString . For local development, it gets the
connection string from the appsettings.json file.
Visual Studio
Visual Studio Code / Visual Studio for Mac
The name value for the database ( Database={Database name} ) will be different for your generated code. The name
value is arbitrary.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-
bc;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
When the app is deployed to a test or production server, an environment variable can be used to set the connection
string to a real database server. See Configuration for more information.
Visual Studio
Visual Studio Code / Visual Studio for Mac
SQL Server Express LocalDB
LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program
development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default,
LocalDB database creates *.mdf files in the C:/Users/<user/> directory.
From the View menu, open SQL Server Object Explorer (SSOX).
namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new RazorPagesMovieContext(
serviceProvider.GetRequiredService<
DbContextOptions<RazorPagesMovieContext>>()))
{
// 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 DB, the seed initializer returns and no movies are added.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;
using System;
namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
try
{
var context = services.
GetRequiredService<RazorPagesMovieContext>();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
});
}
}
A production app would not call Database.Migrate . It's added to the preceding code to prevent the following
exception when Update-Database has not been run:
SqlException: Cannot open database "RazorPagesMovieContext-21" requested by the login. The login failed. Login
failed for user 'user name'.
Test the app
Visual Studio
Visual Studio Code / Visual Studio for Mac
Delete all the records in the DB. You can do this with the delete links in the browser or from SSOX
Force the app to initialize (call 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:
Additional resources
P R E V IO U S : S C A F F O L D E D R A Z O R N E X T: U P D A T IN G T H E
PAGES PAGES
Visual Studio
Visual Studio Code / Visual Studio for Mac
// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is
// needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("RazorPagesMovieContext")));
}
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-
1234;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
When the app is deployed to a test or production server, an environment variable can be used to set the connection
string to a real database server. See Configuration for more information.
Visual Studio
Visual Studio Code
Visual Studio for Mac
namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new RazorPagesMovieContext(
serviceProvider.GetRequiredService<
DbContextOptions<RazorPagesMovieContext>>()))
{
// 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 DB, the seed initializer returns and no movies are added.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RazorPagesMovie.Models;
using System;
using Microsoft.EntityFrameworkCore;
namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context=services.
GetRequiredService<RazorPagesMovieContext>();
context.Database.Migrate();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
A production app would not call Database.Migrate . It's added to the preceding code to prevent the following
exception when Update-Database has not been run:
SqlException: Cannot open database "RazorPagesMovieContext-21" requested by the login. The login failed. Login
failed for user 'user name'.
Test the app
Visual Studio
Visual Studio Code
Visual Studio for Mac
Delete all the records in the DB. You can do this with the delete links in the browser or from SSOX
Force the app to initialize (call 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:
P R E V IO U S : S C A F F O L D E D R A Z O R N E X T: U P D A T IN G T H E
PAGES PAGES
Update the generated pages in an ASP.NET Core app
7/24/2019 • 9 minutes to read • Edit Online
By Rick Anderson
The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate should be Release Date
(two words).
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml file.
<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>
The dynamically-generated links pass the movie ID with a query string (for example, the ?id=1 in
https://localhost:5001/Movies/Details?id=1 ).
Update the Edit, Details, and Delete Razor Pages to use the "{id:int}" route template. Change the page directive for
each of these pages from @page to @page "{id:int}" . Run the app and then view source. The generated HTML
adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the "{id:int}" route template that does not include the integer will return an HTTP 404
(not found) error. For example, http://localhost:5000/Movies/Details will return a 404 error. To make the ID
optional, append ? to the route constraint:
@page "{id:int?}"
With the @page "{id:int}" directive, the break point is never hit. The routing engine returns HTTP 404. Using
@page "{id:int?}" , the OnGetAsync method returns NotFound ( HTTP 404 ).
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
The previous code detects concurrency exceptions when the one client deletes the movie and the other client posts
changes to the movie.
To test the catch block:
Set a breakpoint on catch (DbUpdateConcurrencyException)
Select Edit for a movie, make changes, but don't enter Save.
In another browser window, select the Delete link for the same movie, and then delete the movie.
In the previous browser window, post changes to the movie.
Production code may want to detect concurrency conflicts. See Handle concurrency conflicts for more information.
Posting and binding review
Examine the Pages/Movies/Edit.cshtml.cs file:
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
if (Movie == null)
{
return NotFound();
}
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
When an HTTP GET request is made to the Movies/Edit page (for example, http://localhost:5000/Movies/Edit/2 ):
The OnGetAsync method fetches the movie from the database and returns the Page method.
The Page method renders the Pages/Movies/Edit.cshtml Razor Page. The Pages/Movies/Edit.cshtml file
contains the model directive ( @model RazorPagesMovie.Pages.Movies.EditModel ), which makes the movie model
available on the page.
The Edit form is displayed with the values from the movie.
When the Movies/Edit page is posted:
The form values on the page are bound to the Movie property. The [BindProperty] attribute enables Model
binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state (for example, ReleaseDate cannot be converted to a date), the form is
redisplayed with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST
OnPostAsync method in the Create Razor Page follows a similar pattern to the OnPostAsync method in the Edit
Razor Page.
Additional resources
P R E V IO U S : W O R K IN G W IT H A N E X T: A D D
D A TA B A SE SE A RCH
The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate should be Release Date
(two words).
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
The [Column(TypeName = "decimal(18, 2)")] data annotation enables Entity Framework Core to correctly map
Price to currency in the database. For more information, see Data Types.
DataAnnotations is covered in the next tutorial. The Display attribute specifies what to display for the name of a
field (in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data
(Date), so the time information stored in the field isn't displayed.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml file.
@foreach (var item in Model.Movie) {
<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-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. In the
preceding code, the AnchorTagHelper dynamically generates the HTML href attribute value from the Razor Page
(the route is relative), the asp-page , and the route id ( asp-route-id ). See URL generation for Pages for more
information.
Use View Source from your favorite browser to examine the generated markup. A portion of the generated HTML
is shown below:
<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>
The dynamically-generated links pass the movie ID with a query string (for example, the ?id=1 in
https://localhost:5001/Movies/Details?id=1 ).
Update the Edit, Details, and Delete Razor Pages to use the "{id:int}" route template. Change the page directive for
each of these pages from @page to @page "{id:int}" . Run the app and then view source. The generated HTML
adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the "{id:int}" route template that does not include the integer will return an HTTP 404
(not found) error. For example, http://localhost:5000/Movies/Details will return a 404 error. To make the ID
optional, append ? to the route constraint:
@page "{id:int?}"
With the @page "{id:int}" directive, the break point is never hit. The routing engine returns HTTP 404. Using
@page "{id:int?}" , the OnGetAsync method returns NotFound ( HTTP 404 ).
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
The previous code detects concurrency exceptions when the one client deletes the movie and the other client posts
changes to the movie.
To test the catch block:
Set a breakpoint on catch (DbUpdateConcurrencyException)
Select Edit for a movie, make changes, but don't enter Save.
In another browser window, select the Delete link for the same movie, and then delete the movie.
In the previous browser window, post changes to the movie.
Production code may want to detect concurrency conflicts. See Handle concurrency conflicts for more information.
Posting and binding review
Examine the Pages/Movies/Edit.cshtml.cs file:
public class EditModel : PageModel
{
private readonly RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
if (Movie == null)
{
return NotFound();
}
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
}
When an HTTP GET request is made to the Movies/Edit page (for example, http://localhost:5000/Movies/Edit/2 ):
The OnGetAsync method fetches the movie from the database and returns the Page method.
The Page method renders the Pages/Movies/Edit.cshtml Razor Page. The Pages/Movies/Edit.cshtml file
contains the model directive ( @model RazorPagesMovie.Pages.Movies.EditModel ), which makes the movie model
available on the page.
The Edit form is displayed with the values from the movie.
When the Movies/Edit page is posted:
The form values on the page are bound to the Movie property. The [BindProperty] attribute enables Model
binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state (for example, ReleaseDate cannot be converted to a date), the form is
displayed with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST
OnPostAsync method in the Create Razor Page follows a similar pattern to the OnPostAsync method in the Edit
Razor Page.
Search is added in the next tutorial.
Additional resources
YouTube version of this tutorial
P R E V IO U S : W O R K IN G W IT H A N E X T: A D D
D A TA B A SE SE A RCH
Add search to ASP.NET Core Razor Pages
7/24/2019 • 9 minutes to read • Edit Online
By Rick Anderson
View or download sample code (how to download).
View or download sample code (how to download).
In the following sections, searching movies by genre or name is added.
Add the following highlighted properties to Pages/Movies/Index.cshtml.cs:
SearchString : contains the text users enter in the search text box. SearchString is decorated with the
[BindProperty] attribute. [BindProperty] binds form values and query strings with the same name as the
property. (SupportsGet = true) is required for binding on GET requests.
Genres : contains the list of genres. Genres allows the user to select a genre from the list. SelectList requires
using Microsoft.AspNetCore.Mvc.Rendering;
MovieGenre : contains the specific genre the user selects (for example, "Western").
Genres and MovieGenre are used later in this tutorial.
WARNING
For security reasons, you must opt in to binding GET request data to page model properties. Verify user input before
mapping it to properties. Opting in to GET binding is useful when addressing scenarios which rely on query string or route
values.
To bind a property on GET requests, set the [BindProperty] attribute's SupportsGet property to true :
[BindProperty(SupportsGet = true)]
Update the Index page's OnGetAsync method with the following code:
public async Task OnGetAsync()
{
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 OnGetAsync method creates a LINQ query to select the movies:
// using System.Linq;
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 property is not null or empty, the movies query is modified to filter on the search string:
if (!string.IsNullOrEmpty(SearchString))
{
movies = movies.Where(s => s.Title.Contains(SearchString));
}
The s => s.Title.Contains() code 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 preceding
code). 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 the evaluation of an expression is
delayed until its realized value is iterated over or the ToListAsync method is called. See Query Execution for more
information.
Note: The Contains method is run on the database, not in the C# code. 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.
Navigate to the Movies page and append a query string such as ?searchString=Ghost to the URL (for example,
https://localhost:5001/Movies?searchString=Ghost ). The filtered movies are displayed.
If the following route template is added to the Index page, the search string can be passed as a URL segment (for
example, https://localhost:5001/Movies/Ghost ).
@page "{searchString?}"
The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query string
value. The ? in "{searchString?}" means this is an optional route parameter.
The ASP.NET Core runtime uses model binding to set the value of the SearchString property from the query
string ( ?searchString=Ghost ) or route data ( https://localhost:5001/Movies/Ghost ). Model binding is not case
sensitive.
However, you can't expect users to modify the URL to search for a movie. In this step, UI is added to filter movies. If
you added the route constraint "{searchString?}" , remove it.
Open the Pages/Movies/Index.cshtml file, and add the <form> markup highlighted in the following code:
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
@*Markup removed for brevity.*@
if (!string.IsNullOrEmpty(SearchString))
{
movies = movies.Where(s => s.Title.Contains(SearchString));
}
if (!string.IsNullOrEmpty(MovieGenre))
{
movies = movies.Where(x => x.Genre == MovieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}
The following code is a LINQ query that retrieves all the genres from the database.
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
@*Markup removed for brevity.*@
Additional resources
YouTube version of this tutorial
P R E V IO U S : U P D A T IN G T H E N E X T: A D D IN G A N E W
PAGES F IE L D
SearchString : contains the text users enter in the search text box. SearchString is decorated with the
[BindProperty] attribute. [BindProperty] binds form values and query strings with the same name as the
property. (SupportsGet = true) is required for binding on GET requests.
Genres : contains the list of genres. Genres allows the user to select a genre from the list. SelectList requires
using Microsoft.AspNetCore.Mvc.Rendering;
MovieGenre : contains the specific genre the user selects (for example, "Western").
Genres and MovieGenre are used later in this tutorial.
WARNING
For security reasons, you must opt in to binding GET request data to page model properties. Verify user input before
mapping it to properties. Opting in to GET binding is useful when addressing scenarios which rely on query string or route
values.
To bind a property on GET requests, set the [BindProperty] attribute's SupportsGet property to true :
[BindProperty(SupportsGet = true)]
Update the Index page's OnGetAsync method with the following code:
The first line of the OnGetAsync method creates a LINQ query to select the movies:
// using System.Linq;
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 property is not null or empty, the movies query is modified to filter on the search string:
if (!string.IsNullOrEmpty(SearchString))
{
movies = movies.Where(s => s.Title.Contains(SearchString));
}
The s => s.Title.Contains() code 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 preceding
code). 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 the evaluation of an expression is
delayed until its realized value is iterated over or the ToListAsync method is called. See Query Execution for more
information.
Note: The Contains method is run on the database, not in the C# code. 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.
Navigate to the Movies page and append a query string such as ?searchString=Ghost to the URL (for example,
https://localhost:5001/Movies?searchString=Ghost ). The filtered movies are displayed.
If the following route template is added to the Index page, the search string can be passed as a URL segment (for
example, https://localhost:5001/Movies/Ghost ).
@page "{searchString?}"
The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query string
value. The ? in "{searchString?}" means this is an optional route parameter.
The ASP.NET Core runtime uses model binding to set the value of the SearchString property from the query
string ( ?searchString=Ghost ) or route data ( https://localhost:5001/Movies/Ghost ). Model binding is not case
sensitive.
However, you can't expect users to modify the URL to search for a movie. In this step, UI is added to filter movies. If
you added the route constraint "{searchString?}" , remove it.
Open the Pages/Movies/Index.cshtml file, and add the <form> markup highlighted in the following code:
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
@*Markup removed for brevity.*@
Search by genre
Update the OnGetAsync method with the following code:
if (!string.IsNullOrEmpty(SearchString))
{
movies = movies.Where(s => s.Title.Contains(SearchString));
}
if (!string.IsNullOrEmpty(MovieGenre))
{
movies = movies.Where(x => x.Genre == MovieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}
The following code is a LINQ query that retrieves all the genres from the database.
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
@*Markup removed for brevity.*@
Additional resources
YouTube version of this tutorial
P R E V IO U S : U P D A T IN G T H E N E X T: A D D IN G A N E W
PAGES F IE L D
Add a new field to a Razor Page in ASP.NET Core
7/31/2019 • 10 minutes to read • Edit Online
By Rick Anderson
View or download sample code (how to download).
View or download sample code (how to download).
In this section Entity Framework Code First Migrations is used to:
Add a new field to the model.
Migrate the new field schema change to the database.
When using EF Code First to automatically create a database, Code First:
Adds a table to the database to track whether the schema of the database is in sync with the model classes it was
generated from.
If the model classes aren't in sync with the DB, EF throws an exception.
Automatic verification of schema/model in sync makes it easier to find inconsistent database/code issues.
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Rating)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie)
{
<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>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
This error is caused by the updated Movie model class being different than the schema of the Movie table of the
database. (There's no Rating column in the database table.)
There are a few approaches to resolving the error:
1. Have the Entity Framework automatically drop and re-create the database using the new model class
schema. This approach is convenient early in the development cycle; it allows you to quickly evolve the
model and database schema together. The downside is that you lose existing data in the database. Don't use
this approach on a production database! Dropping the DB on schema changes and using an initializer to
automatically seed the database with test data is often a productive way to develop an app.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of
this approach is that you keep your data. You can make this change either manually or by creating a
database change script.
3. Use Code First Migrations to update the database schema.
For this tutorial, use Code First Migrations.
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 block.
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M,
Rating = "R"
},
Add-Migration Rating
Update-Database
Update-Database
Run the app and verify you can create/edit/display movies with a Rating field. If the database isn't seeded, set a
break point in the SeedData.Initialize method.
Additional resources
YouTube version of this tutorial
P R E V IO U S : A D D IN G N E X T: A D D IN G
SE A RCH V A L ID A T IO N
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Rating)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie)
{
<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>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
The app won't work until the DB is updated to include the new field. If run now, the app throws a SqlException :
SqlException: Invalid column name 'Rating'.
This error is caused by the updated Movie model class being different than the schema of the Movie table of the
database. (There's no Rating column in the database table.)
There are a few approaches to resolving the error:
1. Have the Entity Framework automatically drop and re-create the database using the new model class
schema. This approach is convenient early in the development cycle; it allows you to quickly evolve the
model and database schema together. The downside is that you lose existing data in the database. Don't use
this approach on a production database! Dropping the DB on schema changes and using an initializer to
automatically seed the database with test data is often a productive way to develop an app.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of
this approach is that you keep your data. You can make this change either manually or by creating a
database change script.
3. Use Code First Migrations to update the database schema.
For this tutorial, use Code First Migrations.
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 block.
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M,
Rating = "R"
},
Add-Migration Rating
Update-Database
Update-Database
Run the app and verify you can create/edit/display movies with a Rating field. If the database isn't seeded, set a
break point in the SeedData.Initialize method.
Additional resources
YouTube version of this tutorial
P R E V IO U S : A D D IN G N E X T: A D D IN G
SE A RCH V A L ID A T IO N
Add validation to an ASP.NET Core Razor Page
7/24/2019 • 9 minutes to read • Edit Online
By Rick Anderson
In this section, validation logic is added to the Movie model. The validation rules are enforced any time a user
creates or edits a movie.
Validation
A key tenet of software development is called DRY ("Don't Repeat Yourself"). Razor Pages encourages
development where functionality is specified once, and it's reflected throughout the app. DRY can help:
Reduce the amount of code in an app.
Make the code less error prone, and easier to test and maintain.
The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle.
Validation rules are declaratively specified in one place (in the model class), and the rules are enforced everywhere
in the app.
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; }
}
The validation attributes specify behavior that you want to enforce on the model properties they're applied to:
The Required and MinimumLength attributes indicate that a property must have a value; but nothing prevents
a user from entering white space to satisfy this validation.
The RegularExpression attribute is used to limit what characters can be input. In the preceding code,
"Genre":
Must only use letters.
The first letter is required to be uppercase. White space, numbers, and special characters are not allowed.
The RegularExpression "Rating":
Requires that the first character be an uppercase letter.
Allows special characters and numbers in subsequent spaces. "PG -13" is valid for a rating, but fails for a
"Genre".
The Range attribute constrains a value to within a specified range.
The StringLength attribute lets you set the maximum length of a string property, and optionally its
minimum length.
Value types (such as decimal , int , float , DateTime ) are inherently required and don't need the
[Required] attribute.
Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures
that you can't forget to validate something and inadvertently let bad data into the database.
Validation Error UI in Razor Pages
Run the app and navigate to Pages/Movies.
Select the Create New link. Complete the form with some invalid values. When jQuery client-side validation
detects the error, it displays an error message.
NOTE
You may not be able to enter decimal commas in decimal fields. To support jQuery validation for non-English locales that use
a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. This GitHub
issue 4076 for instructions on adding decimal comma.
Notice how the form has automatically rendered a validation error message in each field containing an invalid
value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (when a user has
JavaScript disabled).
A significant benefit is that no code changes were necessary in the Create or Edit pages. Once DataAnnotations
were applied to the model, the validation UI was enabled. The Razor Pages created in this tutorial automatically
picked up the validation rules (using validation attributes on the properties of the Movie model class). Test
validation using the Edit page, the same validation is applied.
The form data isn't posted to the server until there are no client-side validation errors. Verify form data isn't posted
by one or more of the following approaches:
Put a break point in the OnPostAsync method. Submit the form (select Create or Save). The break point is never
hit.
Use the Fiddler tool.
Use the browser developer tools to monitor network traffic.
Server-side validation
When JavaScript is disabled in the browser, submitting the form with errors will post to the server.
Optional, test server-side validation:
Disable JavaScript in the browser. You can disable JavaScript using browser's developer tools. If you can't
disable JavaScript in the browser, try another browser.
Set a break point in the OnPostAsync method of the Create or Edit page.
Submit a form with invalid data.
Verify the model state is invalid:
if (!ModelState.IsValid)
{
return Page();
}
The following code shows a portion of the Create.cshtml page scaffolded earlier in the tutorial. It's used by the
Create and Edit pages to display the initial form and to redisplay the form in the event of an error.
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
The Input Tag Helper uses the DataAnnotations attributes and produces HTML attributes needed for jQuery
Validation on the client-side. The Validation Tag Helper displays validation errors. See Validation for more
information.
The Create and Edit pages have no validation rules in them. The validation rules and the error strings are specified
only in the Movie class. These validation rules are automatically applied to Razor Pages that edit the Movie model.
When validation logic needs to change, it's done only in the model. Validation is applied consistently throughout the
application (validation logic is defined in one place). Validation in one place helps keep the code clean, and makes it
easier to maintain and update.
[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
The DataType attributes only provide hints for the view engine to format the data (and supplies attributes such as
<a> for URL's and <a href="mailto:EmailAddress.com"> for email). Use the RegularExpression attribute to validate
the format of the data. The DataType attribute is used to specify a data type that's more specific than the database
intrinsic type. DataType attributes are not validation attributes. In the sample application, only the date is displayed,
without time.
The DataType Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency,
EmailAddress, and more. The DataType attribute can also enable the application to automatically provide type-
specific features. For example, a mailto: link can be created for DataType.EmailAddress . A date selector can be
provided for DataType.Date in browsers that support HTML5. The DataType attributes emits HTML 5 data-
(pronounced data dash) attributes that HTML 5 browsers consume. The DataType attributes do not provide any
validation.
DataType.Date doesn't specify the format of the date that's displayed. By default, the data field is displayed
according to the default formats based on the server's CultureInfo .
The [Column(TypeName = "decimal(18, 2)")] data annotation is required so Entity Framework Core can correctly
map Price to currency in the database. For more information, see Data Types.
The DisplayFormat attribute is used to explicitly specify the date format:
The ApplyFormatInEditMode setting specifies that the formatting should be applied when the value is displayed for
editing. You might not want that behavior for some fields. For example, in currency values, you probably don't want
the currency symbol in the edit UI.
The DisplayFormat attribute can be used by itself, but it's generally a good idea to use the DataType attribute. The
DataType attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the
following benefits that you don't get with DisplayFormat:
The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate
currency symbol, email links, etc.)
By default, the browser will render data using the correct format based on your locale.
The DataType attribute can enable the ASP.NET Core framework to choose the right field template to render
the data. The DisplayFormat if used by itself uses the string template.
Note: jQuery validation doesn't work with the Range attribute and DateTime . For example, the following code will
always display a client-side validation error, even when the date is in the specified range:
It's generally not a good practice to compile hard dates in your models, so using the Range attribute and DateTime
is discouraged.
The following code shows combining attributes on one line:
public class Movie
{
public int ID { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}
Get started with Razor Pages and EF Core shows advanced EF Core operations with Razor Pages.
Apply migrations
The DataAnnotations applied to the class change the schema. For example, the DataAnnotations applied to the
Title field:
Visual Studio
Visual Studio Code / Visual Studio for Mac
The Movie table currently has the following schema:
The preceding schema changes don't cause EF to throw an exception. However, create a migration so the schema is
consistent with the model.
From the Tools menu, select NuGet Package Manager > Package Manager Console. In the PMC, enter the
following commands:
Add-Migration New_DataAnnotations
Update-Database
Update-Database runs the Up methods of the New_DataAnnotations class. Examine the Up method:
migrationBuilder.AlterColumn<string>(
name: "Rating",
table: "Movie",
maxLength: 5,
nullable: false,
oldClrType: typeof(string),
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Genre",
table: "Movie",
maxLength: 30,
nullable: false,
oldClrType: typeof(string),
oldNullable: true);
}
Publish to Azure
For information on deploying to Azure, see Tutorial: Build an ASP.NET Core app in Azure with SQL Database.
Thanks for completing this introduction to Razor Pages. Get started with Razor Pages and EF Core is an excellent
follow up to this tutorial.
Additional resources
Tag Helpers in forms in ASP.NET Core
Globalization and localization in ASP.NET Core
Tag Helpers in ASP.NET Core
Author Tag Helpers in ASP.NET Core
YouTube version of this tutorial
P R E V IO U S : A D D IN G A N E W
F IE L D
Create a web app with ASP.NET Core MVC
4/26/2019 • 2 minutes to read • Edit Online
This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET
Core web development, consider the Razor Pages version of this tutorial, which provides an easier starting point.
The tutorial series includes the following:
1. Get started
2. Add a controller
3. Add a view
4. Add a model
5. Work with SQL Server LocalDB
6. Controller methods and views
7. Add search
8. Add a new field
9. Add validation
10. Examine the Details and Delete methods
Get started with ASP.NET Core MVC
8/6/2019 • 11 minutes to read • Edit Online
By Rick Anderson
This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET
Core web development, consider the Razor Pages version of this tutorial, which provides an easier starting point.
This tutorial teaches the basics of building an ASP.NET Core MVC web app.
The app manages a database of movie titles. You learn how to:
Create a web app.
Add and scaffold a model.
Work with a database.
Add search and validation.
At the end, you have an app that can manage and display movie data.
View or download sample code (how to download).
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 3.0 Preview
You can debug the app by selecting the IIS Express button
The following image shows the app:
Visual Studio
Visual Studio Code
Visual Studio for Mac
NEXT
This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET
Core web development, consider the Razor Pages version of this tutorial, which provides an easier starting point.
This tutorial teaches the basics of building an ASP.NET Core MVC web app.
The app manages a database of movie titles. You learn how to:
Create a web app.
Add and scaffold a model.
Work with a database.
Add search and validation.
At the end, you have an app that can manage and display movie data.
View or download sample code (how to download).
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later
WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work with
Visual Studio.
Name the project MvcMovie and select Create. It's important to name the project MvcMovie so when
you copy code, the namespace will match.
Select Web Application(Model-View-Controller), and then select Create.
Visual Studio used the default template for the MVC project you just created. You have a working app right now by
entering a project name and selecting a few options. This is a basic starter project, and it's a good place to start.
Run the app
Visual Studio
Visual Studio Code
Visual Studio for Mac
Select Ctrl-F5 to run the app in non-debug mode.
Visual Studio displays the following dialog:
Select Accept to consent to tracking. This app doesn't track personal information. The template generated
code includes assets to help meet General Data Protection Regulation (GDPR ).
NEXT
Add a controller to an ASP.NET Core MVC app
8/6/2019 • 12 minutes to read • Edit Online
By Rick Anderson
The Model-View -Controller (MVC ) architectural pattern separates an app into three main components: Model,
View, and Controller. The MVC pattern helps you create apps that are more testable and easier to update than
traditional monolithic apps. MVC -based apps contain:
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 handle browser requests. They retrieve model data and 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 route data and query-string values, and
passes these values to the model. The model might use these values to query the database. For example,
https://localhost:5001/Home/Privacy has route data of Home (the controller ) and Privacy (the action
method to call on the home controller). https://localhost:5001/Movies/Edit/5 is a request to edit the movie
with ID=5 using the movie controller. Route data is explained later in the tutorial.
The MVC pattern helps you create apps that separate the different aspects of the app (input logic, business logic,
and UI logic), while providing a loose coupling between these elements. The pattern specifies where each kind of
logic should be located in the app. The UI logic belongs in the view. Input logic belongs in the controller. Business
logic belongs in the model. This separation helps you manage complexity when you build an app, because it
enables you to 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.
We cover these concepts in this tutorial series and show you how to use them to build 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 Solution Explorer, right-click Controllers > Add > Controller
In the Add Scaffold dialog box, select MVC Controller - Empty
In the Add Empty MVC Controller dialog, enter HelloWorldController and select ADD.
Replace the contents of Controllers/HelloWorldController.cs with the following:
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/
//
// GET: /HelloWorld/Welcome/
Every public method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a
string. Note the comments preceding each method.
An HTTP endpoint is a targetable URL in the web application, such as https://localhost:5001/HelloWorld , and
combines the protocol used: HTTPS , the network location of the web server (including the TCP port):
localhost:5001 and the target URI HelloWorld .
The first comment states this is an HTTP GET method that's invoked by appending /HelloWorld/ to the base URL.
The second comment specifies an HTTP GET method that's invoked by appending /HelloWorld/Welcome/ to the
URL. Later on in the tutorial the scaffolding engine is used to generate HTTP POST methods which update data.
Run the app in non-debug mode and 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]
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
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.
The first URL segment determines the controller class to run. So localhost:{PORT}/HelloWorld maps to the
HelloWorldController class. The second part of the URL segment determines the action method on the class. So
localhost:{PORT}/HelloWorld/Index would cause the Index method of the HelloWorldController class to run.
Notice that you only had to browse to localhost:{PORT}/HelloWorld and the Index method was called by default.
That's because Index is the default method that will be called on a controller if a method name isn't explicitly
specified. The third part of the URL segment ( id ) is for route data. Route data is explained later in the tutorial.
Browse to https://localhost:{PORT}/HelloWorld/Welcome . The Welcome method runs and returns the string
This is the Welcome action method... . For this URL, the controller is HelloWorld and Welcome is the action
method. You haven't used the [Parameters] part of the URL yet.
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.
// 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}");
}
(Replace {PORT} with your port number.) You can try different values for name and numtimes in the URL. The
MVC model binding system automatically maps the named parameters from the query string in the address bar to
parameters in your method. See Model Binding for more information.
In the image above, the URL segment ( Parameters ) isn't used, the name and numTimes parameters are passed as
query strings. The ? (question mark) in the above URL is a separator, and the query strings follow. The &
character separates query strings.
Replace the Welcome method with the following code:
This time the third URL segment matched the route parameter id . The Welcome method contains a parameter
id that matched the URL template in the MapControllerRoute method. The trailing ? (in id? ) indicates the id
parameter is optional.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
In these examples the controller has been doing the "VC" portion of MVC - that is, the View and the Controller
work. The controller is returning HTML directly. Generally you don't want controllers returning HTML directly,
since that becomes very cumbersome to code and maintain. Instead you typically use a separate Razor view
template file to generate the HTML response. You do that in the next tutorial.
P R E V IO U S NEXT
The Model-View -Controller (MVC ) architectural pattern separates an app into three main components: Model,
View, and Controller. The MVC pattern helps you create apps that are more testable and easier to update than
traditional monolithic apps. MVC -based apps contain:
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 handle browser requests. They retrieve model data and 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 route data and query-string values, and
passes these values to the model. The model might use these values to query the database. For example,
https://localhost:5001/Home/About has route data of Home (the controller ) and About (the action method to
call on the home controller). https://localhost:5001/Movies/Edit/5 is a request to edit the movie with ID=5
using the movie controller. Route data is explained later in the tutorial.
The MVC pattern helps you create apps that separate the different aspects of the app (input logic, business logic,
and UI logic), while providing a loose coupling between these elements. The pattern specifies where each kind of
logic should be located in the app. The UI logic belongs in the view. Input logic belongs in the controller. Business
logic belongs in the model. This separation helps you manage complexity when you build an app, because it
enables you to 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.
We cover these concepts in this tutorial series and show you how to use them to build 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 Solution Explorer, right-click Controllers > Add > Controller
In the Add Scaffold dialog box, select MVC Controller - Empty
In the Add Empty MVC Controller dialog, enter HelloWorldController and select ADD.
Replace the contents of Controllers/HelloWorldController.cs with the following:
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/
//
// GET: /HelloWorld/Welcome/
Every public method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a
string. Note the comments preceding each method.
An HTTP endpoint is a targetable URL in the web application, such as https://localhost:5001/HelloWorld , and
combines the protocol used: HTTPS , the network location of the web server (including the TCP port):
localhost:5001 and the target URI HelloWorld .
The first comment states this is an HTTP GET method that's invoked by appending /HelloWorld/ to the base URL.
The second comment specifies an HTTP GET method that's invoked by appending /HelloWorld/Welcome/ to the
URL. Later on in the tutorial the scaffolding engine is used to generate HTTP POST methods which update data.
Run the app in non-debug mode and 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]
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{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.
The first URL segment determines the controller class to run. So localhost:{PORT}/HelloWorld maps to the
HelloWorldController class. The second part of the URL segment determines the action method on the class. So
localhost:{PORT}/HelloWorld/Index would cause the Index method of the HelloWorldController class to run.
Notice that you only had to browse to localhost:{PORT}/HelloWorld and the Index method was called by default.
This is because Index is the default method that will be called on a controller if a method name isn't explicitly
specified. The third part of the URL segment ( id ) is for route data. Route data is explained later in the tutorial.
Browse to https://localhost:{PORT}/HelloWorld/Welcome . The Welcome method runs and returns the string
This is the Welcome action method... . For this URL, the controller is HelloWorld and Welcome is the action
method. You haven't used the [Parameters] part of the URL yet.
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.
// 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}");
}
(Replace {PORT} with your port number.) You can try different values for name and numtimes in the URL. The
MVC model binding system automatically maps the named parameters from the query string in the address bar to
parameters in your method. See Model Binding for more information.
In the image above, the URL segment ( Parameters ) isn't used, the name and numTimes parameters are passed as
query strings. The ? (question mark) in the above URL is a separator, and the query strings follow. The &
character separates query strings.
Replace the Welcome method with the following code:
This time the third URL segment matched the route parameter id . The Welcome method contains a parameter
id that matched the URL template in the MapRoute method. The trailing ? (in id? ) indicates the id parameter
is optional.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
In these examples the controller has been doing the "VC" portion of MVC - that is, the view and controller work.
The controller is returning HTML directly. Generally you don't want controllers returning HTML directly, since that
becomes very cumbersome to code and maintain. Instead you typically use a separate Razor view template file to
help generate the HTML response. You do that in the next tutorial.
P R E V IO U S NEXT
Add a view to an ASP.NET Core MVC app
8/6/2019 • 16 minutes to read • Edit Online
By Rick Anderson
In this section you modify the HelloWorldController class to use Razor view files to cleanly encapsulate the process
of generating HTML responses to a client.
You create a view template file using Razor. Razor-based view templates have a .cshtml file extension. They provide
an elegant way to create HTML output with C#.
Currently the method returns a string with a message that's hard-coded in the controller class. In the
Index
HelloWorldController class, replace the Index method with the following code:
The preceding code calls the controller's View method. It uses a view template to generate an HTML response.
Controller methods (also known as action methods), such as the Index method above, 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
In the search box in the upper-right, enter view
Select Razor View
Keep the Name box value, Index.cshtml.
Select Add
Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the following:
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
Change the title, footer, and menu link in the layout file
Replace the content of the Views\Shared_Layout.cshtml file with the following markup. The changes are
highlighted:
<!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.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 flex-sm-row-reverse">
<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>
Select the Home link and notice that the title and anchor text also display Movie App. We were able to make the
change once in the layout template and have all pages on the site reflect the new link text and new title.
Examine the Views/_ViewStart.cshtml file:
@{
Layout = "_Layout";
}
The Views/_ViewStart.cshtml file brings in the Views/Shared/_Layout.cshtml file to each view. The Layout property
can be used to set a different layout view, or set it to null so no layout file will be used.
Change the title and <h2> element of the Views/HelloWorld/Index.cshtml view file:
@{
ViewData["Title"] = "Movie List";
}
The title and <h2> element are slightly different so you can see which bit of code changes the display.
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:
Save the change and navigate to https://localhost:{PORT}/HelloWorld . Notice that the browser title, the primary
heading, and the secondary headings have changed. (If you don't see changes in the browser, you might be viewing
cached content. Press Ctrl+F5 in your 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.
The content in the Index.cshtml view template is merged with the Views/Shared/_Layout.cshtml view template. A
single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all
of the pages in an app. To learn more, see Layout.
Our little bit of "data" (in this case the "Hello from our View Template!" message) is hard-coded, though. The MVC
application has a "V" (view ) and you've got a "C" (controller), but no "M" (model) yet.
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.
Create a Welcome view template named Views/HelloWorld/Welcome.cshtml.
You'll create a loop in the Welcome.cshtml view template that displays "Hello" NumTimes . Replace the contents of
Views/HelloWorld/Welcome.cshtml with the following:
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
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 the sample above, the ViewData dictionary was used to pass data from the controller to a view. Later in the
tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is
generally much preferred over the ViewData dictionary approach. See When to use ViewBag, ViewData, or
TempData for more information.
In the next tutorial, a database of movies is created.
P R E V IO U S NEXT
In this section you modify the HelloWorldController class to use Razor view files to cleanly encapsulate the process
of generating HTML responses to a client.
You create a view template file using Razor. Razor-based view templates have a .cshtml file extension. They provide
an elegant way to create HTML output with C#.
Currently the method returns a string with a message that's hard-coded in the controller class. In the
Index
HelloWorldController class, replace the Index method with the following code:
The preceding code calls the controller's View method. It uses a view template to generate an HTML response.
Controller methods (also known as action methods), such as the Index method above, 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
In the search box in the upper-right, enter view
Select Razor View
Keep the Name box value, Index.cshtml.
Select Add
Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the following:
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
Change the title, footer, and menu link in the layout file
In the title and footer elements, change MvcMovie to Movie App .
Change the anchor element
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a> to
<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a> .
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute"
crossorigin="anonymous"
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
</environment>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<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 flex-sm-row-reverse">
<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">
<partial name="_CookieConsentPartial" />
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
</script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>
In the preceding markup, the asp-area anchor Tag Helper attribute was omitted because this app is not using
Areas.
Note: The Movies controller has not been implemented. At this point, the Movie App link is not functional.
Save your 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 and notice that the title and anchor text also display Movie App. We were able to make the
change once in the layout template and have all pages on the site reflect the new link text and new title.
Examine the Views/_ViewStart.cshtml file:
@{
Layout = "_Layout";
}
The Views/_ViewStart.cshtml file brings in the Views/Shared/_Layout.cshtml file to each view. The Layout property
can be used to set a different layout view, or set it to null so no layout file will be used.
Change the title and <h2> element of the Views/HelloWorld/Index.cshtml view file:
@{
ViewData["Title"] = "Movie List";
}
The title and <h2> element are slightly different so you can see which bit of code changes the display.
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:
Save the change and navigate to https://localhost:{PORT}/HelloWorld . Notice that the browser title, the primary
heading, and the secondary headings have changed. (If you don't see changes in the browser, you might be viewing
cached content. Press Ctrl+F5 in your 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.
Also notice how the content in the Index.cshtml view template was merged with the Views/Shared/_Layout.cshtml
view template and a single HTML response was sent to the browser. Layout templates make it really easy to make
changes that apply across all of the pages in your application. To learn more see Layout.
Our little bit of "data" (in this case the "Hello from our View Template!" message) is hard-coded, though. The MVC
application has a "V" (view ) and you've got a "C" (controller), but no "M" (model) yet.
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.
Create a Welcome view template named Views/HelloWorld/Welcome.cshtml.
You'll create a loop in the Welcome.cshtml view template that displays "Hello" NumTimes . Replace the contents of
Views/HelloWorld/Welcome.cshtml with the following:
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
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 the sample above, the ViewData dictionary was used to pass data from the controller to a view. Later in the
tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is
generally much preferred over the ViewData dictionary approach. See When to use ViewBag, ViewData, or
TempData for more information.
In the next tutorial, a database of movies is created.
P R E V IO U S NEXT
Add a model to an ASP.NET Core MVC app
6/11/2019 • 12 minutes to read • Edit Online
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; }
}
}
In the Add Scaffold dialog, select MVC Controller with views, using Entity Framework > Add.
SqlException: Cannot open database "MvcMovieContext-<GUID removed>" requested by the login. The login failed.
Login failed for user 'Rick'.
You need to create the database, and you use the EF Core Migrations feature to do that. Migrations lets you create
a database that matches your data model and update the database schema when your data model changes.
Initial migration
In this section, the following tasks are completed:
Add an initial migration.
Update the database with the initial migration.
Visual Studio
Visual Studio Code / Visual Studio for Mac
1. From the Tools menu, select NuGet Package Manager > Package Manager Console (PMC ).
Add-Migration Initial
Update-Database
The Add-Migration command generates code to create the initial database schema.
The database schema is based on the model specified in the MvcMovieContext class. The Initial argument
is the migration name. Any name can be used, but by convention, a name that describes the migration is
used. For more information, see Tutorial: Using the migrations feature - ASP.NET MVC with EF Core.
The Update-Database command runs the Up method in the Migrations/{time-stamp }_InitialCreate.cs file,
which creates the database.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
The MvcMovieContext coordinates EF Core functionality (Create, Read, Update, Delete, etc.) for the Movie model.
The data context ( MvcMovieContext ) is derived from Microsoft.EntityFrameworkCore.DbContext. The data context
specifies which entities are included in the data model:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace MvcMovie.Models
{
public class MvcMovieContext : DbContext
{
public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
: base(options)
{
}
The preceding code creates a DbSet<Movie> property for the entity set. In Entity Framework terminology, an
entity set typically corresponds to a database table. An entity corresponds to a row in the table.
The name of the connection string is passed in to the context by calling a method on a DbContextOptions object.
For local development, the ASP.NET Core configuration system reads the connection string from the
appsettings.json file.
Test the app
Run the app and append /Movies to the URL in the browser ( http://localhost:port/movies ).
If you get a database exception similar to the following:
SqlException: Cannot open database "MvcMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.
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.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
The preceding highlighted code shows the movie database context being added to the Dependency Injection
container:
services.AddDbContext<MvcMovieContext>(options => specifies the database to use and the connection string.
=> is a lambda operator
The constructor uses Dependency Injection to inject the database context ( MvcMovieContext ) into the controller. The
database context is used in each of the CRUD methods in the controller.
Strongly typed models and the @model keyword
Earlier in this tutorial, you saw how a controller can pass data or objects to a view using the ViewData dictionary.
The ViewData dictionary is a dynamic object that provides a convenient late-bound way to pass information to a
view.
MVC also provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables
better compile time checking of your code. The scaffolding mechanism used this approach (that is, passing a
strongly typed model) with the MoviesController class and views when it created the methods and views.
Examine the generated Details method in the Controllers/MoviesController.cs file:
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The id parameter is generally passed as route data. For example https://localhost:5001/movies/details/1 sets:
The controller to the movies controller (the first URL segment).
The action to details (the second URL segment).
The id to 1 (the last URL segment).
You can also pass in the id with a query string as follows:
https://localhost:5001/movies/details?id=1
The id parameter is defined as a nullable type ( int? ) in case an ID value isn't provided.
A lambda expression is passed in to FirstOrDefaultAsync to select movie entities that match the route data or
query string value.
If a movie is found, an instance of the Movie model is passed to the Details view:
return View(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>
By including a @model statement at the top of the view file, you can specify the type of object that the view expects.
When you created the movie controller, the following @model statement was automatically included at the top of
the Details.cshtml file:
@model MvcMovie.Models.Movie
This @model directive allows you to access the movie that the controller passed to the view by using a Model object
that's 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:
// GET: Movies
public async Task<IActionResult> Index()
{
return View(await _context.Movie.ToListAsync());
}
When you created the movies controller, scaffolding automatically included the following @model statement at the
top of the Index.cshtml file:
@model IEnumerable<MvcMovie.Models.Movie>
The @model directive allows you to access 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:
@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, this means that you get compile time checking of the code:
Additional resources
Tag Helpers
Globalization and localization
P R E V IO U S A D D IN G A N E X T W O R K IN G W IT H
V IE W SQL
Work with SQL in ASP.NET Core
4/26/2019 • 4 minutes to read • Edit Online
By Rick Anderson
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
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
The ASP.NET Core Configuration system reads the ConnectionString . For local development, it gets the
connection string from the appsettings.json file:
"ConnectionStrings": {
"MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-
2;Trusted_Connection=True;MultipleActiveResultSets=true"
}
When you deploy the app to a test or production server, you can use an environment variable or another approach
to set the connection string to a real SQL Server. See Configuration for more information.
Visual Studio
Visual Studio Code / Visual Studio for Mac
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 DB, the seed initializer returns and no movies are added.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;
using MvcMovie;
namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context = services.GetRequiredService<MvcMovieContext>();
context.Database.Migrate();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
P R E V IO U S NEXT
Controller methods and views in ASP.NET Core
7/11/2019 • 9 minutes to read • Edit Online
By Rick Anderson
We have a good start to the movie app, but the presentation isn't ideal, for example, ReleaseDate should be two
words.
Open the Models/Movie.cs file and add the highlighted lines shown below:
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; }
We cover DataAnnotations in the next tutorial. The Display attribute specifies what to display for the name of a field
(in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data (Date),
so the time information stored in the field isn't displayed.
The [Column(TypeName = "decimal(18, 2)")] data annotation is required so Entity Framework Core can correctly
map Price to currency in the database. For more information, see Data Types.
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.
<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:
<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
ASP.NET Core translates https://localhost:5001/Movies/Edit/4 into a request to the Edit action method of the
Movies controller with the parameter Id of 4. ( Controller methods are also known as action methods.)
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.
// 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:
// 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);
}
// 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:
// 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.
[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);
}
// 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 HttpPost attribute specifies that this Edit method can be invoked only for POST requests. You could apply
the [HttpGet] attribute to the first edit method, but that's not necessary because [HttpGet] is the default.
The ValidateAntiForgeryToken attribute is used to prevent forgery of a request and is paired up with an anti-
forgery token generated in the edit view file (Views/Movies/Edit.cshtml). The edit view file generates the anti-
forgery token with the Form Tag Helper.
<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 Anti-Request
Forgery.
The method takes the movie ID parameter, looks up the movie using the Entity Framework
HttpGet Edit
FindAsync method, and returns the selected movie to the Edit view. If a movie cannot be found, NotFound ( HTTP
404) is returned.
// 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:
@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.
<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="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-
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.
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);
}
// 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 [ValidateAntiForgeryToken] attribute validates the hidden XSRF token generated by the anti-forgery token
generator in the Form Tag Helper
The model binding system takes the posted form values and creates a Movie object that's passed as the movie
parameter. The ModelState.IsValid method 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.
Additional resources
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
Anti-Request Forgery
Protect your controller from over-posting
ViewModels
Form Tag Helper
Input Tag Helper
Label Tag Helper
Select Tag Helper
Validation Tag Helper
P R E V IO U S NEXT
Add search to an ASP.NET Core MVC app
8/7/2019 • 7 minutes to read • Edit Online
By Rick Anderson
In this section, you add search capability to the Index action method that lets you search movies by genre or
name.
Update the Index method found inside Controllers/MoviesController.cs with the following code:
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:
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:
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.
Navigate to /Movies/Index . Append a query string such as ?searchString=Ghost to the URL. The filtered movies
are displayed.
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.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
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 :
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:
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.
You could add the following [HttpPost] Index method.
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
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.
@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.
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; }
}
}
// 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.
@{
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>
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.Movies[0].Title)
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.
Test the app by searching by genre, by movie title, and by both:
P R E V IO U S NEXT
Add a new field to an ASP.NET Core MVC app
8/2/2019 • 5 minutes to read • Edit Online
By Rick Anderson
In this section Entity Framework Code First Migrations is used to:
Add a new field to the model.
Migrate the new field to the database.
When EF Code First is used to automatically create a database, Code First:
Adds a table to the database to track the schema of the database.
Verifies the database is in sync with the model classes it was generated from. If they aren't in sync, EF throws an
exception. This makes it easier to find inconsistent database/code issues.
[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.
Edit the /Views/Movies/Index.cshtml file and add a Rating field:
<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>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
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:
SqlException: Invalid column name 'Rating'.
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.)
There are a few approaches to resolving the error:
1. Have the Entity Framework automatically drop and re-create the database based on the new model class
schema. This approach is very convenient early in the development cycle when you're doing active
development on a test database; it allows you to quickly evolve the model and database schema together.
The downside, though, is that you lose existing data in the database — so you don't want to use this
approach on a production database! Using an initializer to automatically seed a database with test data is
often a productive way to develop an application. This is a good approach for early development and when
using SQLite.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of
this approach is that you keep your data. You can make this change either manually or by creating a
database change script.
3. Use Code First Migrations to update the database schema.
For this tutorial, Code First Migrations is used.
Visual Studio
Visual Studio Code / Visual Studio for Mac
From the Tools menu, select NuGet Package Manager > Package Manager Console.
Add-Migration Rating
Update-Database
The command tells the migration framework to examine the current Movie model with the current
Add-Migration
Movie DB schema and create the necessary code to migrate the DB to the new model.
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/display movies with a Rating field. You should add the Rating field to
the Edit , Details , and Delete view templates.
P R E V IO U S NEXT
Add validation to an ASP.NET Core MVC app
7/11/2019 • 9 minutes to read • Edit Online
By Rick Anderson
In this section:
Validation logic is added to the Movie model.
You ensure that the validation rules are enforced any time a user creates or edits a movie.
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; }
}
The validation attributes specify behavior that you want to enforce on the model properties they're applied to:
The Required and MinimumLength attributes indicate that a property must have a value; but nothing prevents
a user from entering white space to satisfy this validation.
The RegularExpression attribute is used to limit what characters can be input. In the preceding code,
"Genre":
Must only use letters.
The first letter is required to be uppercase. White space, numbers, and special characters are not allowed.
The RegularExpression "Rating":
Requires that the first character be an uppercase letter.
Allows special characters and numbers in subsequent spaces. "PG -13" is valid for a rating, but fails for a
"Genre".
The Range attribute constrains a value to within a specified range.
The StringLength attribute lets you set the maximum length of a string property, and optionally its
minimum length.
Value types (such as decimal , int , float , DateTime ) are inherently required and don't need the
[Required] attribute.
Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures
that you can't forget to validate something and inadvertently let bad data into the database.
Validation Error UI
Run the app and navigate to the Movies controller.
Tap the Create New link to add a new movie. Fill out the form with some invalid values. As soon as jQuery client
side validation detects the error, it displays an error message.
NOTE
You may not be able to enter decimal commas in decimal fields. To support jQuery validation for non-English locales that use
a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. This GitHub
issue 4076 for instructions on adding decimal comma.
Notice how the form has automatically rendered an appropriate validation error message in each field containing
an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a
user has JavaScript disabled).
A significant benefit is that you didn't need to change a single line of code in the MoviesController class or in the
Create.cshtml view in order to enable this validation UI. The controller and views you created earlier in this tutorial
automatically picked up the validation rules that you specified by using validation attributes on the properties of the
Movie model class. Test validation using the Edit action method, and the same validation is applied.
The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a
break point in the HTTP Post method, by using the Fiddler tool , or the F12 Developer tools.
// GET: Movies/Create
public IActionResult Create()
{
return View();
}
// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(movie);
}
The first (HTTP GET) Create action method displays the initial Create form. The second ( [HttpPost] ) version
handles the form post. The second Create method (The [HttpPost] version) calls ModelState.IsValid to check
whether the movie has any validation errors. Calling this method evaluates any validation attributes that have been
applied to the object. If the object has validation errors, the Create method re-displays the form. If there are no
errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the server
when there are validation errors detected on the client side; the second Create method is never called when there
are client side validation errors. If you disable JavaScript in your browser, client validation is disabled and you can
test the HTTP POST Create method ModelState.IsValid detecting any validation errors.
You can set a break point in the [HttpPost] Create method and verify the method is never called, client side
validation won't submit the form data when validation errors are detected. If you disable JavaScript in your
browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.
The following image shows how to disable JavaScript in the FireFox browser.
The following image shows how to disable JavaScript in the Chrome browser.
After you disable JavaScript, post invalid data and step through the debugger.
The portion of the Create.cshtml view template is shown in the following markup:
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<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>
The preceding markup is used by the action methods to display the initial form and to redisplay it in the event of an
error.
The Input Tag Helper uses the DataAnnotations attributes and produces HTML attributes needed for jQuery
Validation on the client side. The Validation Tag Helper displays validation errors. See Validation for more
information.
What's really nice about this approach is that neither the controller nor the Create view template knows anything
about the actual validation rules being enforced or about the specific error messages displayed. The validation rules
and the error strings are specified only in the Movie class. These same validation rules are automatically applied to
the Edit view and any other views templates you might create that edit your model.
When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the
model (in this example, the Movie class). You won't have to worry about different parts of the application being
inconsistent with how the rules are enforced — all validation logic will be defined in one place and used
everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be
fully honoring the DRY principle.
[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
The DataType attributes only provide hints for the view engine to format the data (and supplies elements/attributes
such as <a> for URL's and <a href="mailto:EmailAddress.com"> for email. You can use the RegularExpression
attribute to validate the format of the data. The DataType attribute is used to specify a data type that's more specific
than the database intrinsic type, they're not validation attributes. In this case we only want to keep track of the date,
not the time. The DataType Enumeration provides for many data types, such as Date, Time, PhoneNumber,
Currency, EmailAddress and more. The DataType attribute can also enable the application to automatically provide
type-specific features. For example, a mailto: link can be created for DataType.EmailAddress , and a date selector
can be provided for DataType.Date in browsers that support HTML5. The DataType attributes emit HTML 5 data-
(pronounced data dash) attributes that HTML 5 browsers can understand. The DataType attributes do not provide
any validation.
DataType.Date doesn't specify the format of the date that's displayed. By default, the data field is displayed
according to the default formats based on the server's CultureInfo .
The DisplayFormat attribute is used to explicitly specify the date format:
The ApplyFormatInEditMode setting specifies that the formatting should also be applied when the value is displayed
in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably
don't want the currency symbol in the text box for editing.)
You can use the DisplayFormat attribute by itself, but it's generally a good idea to use the DataType attribute. The
DataType attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the
following benefits that you don't get with DisplayFormat:
The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate
currency symbol, email links, etc.)
By default, the browser will render data using the correct format based on your locale.
The DataType attribute can enable MVC to choose the right field template to render the data (the
DisplayFormat if used by itself uses the string template).
NOTE
jQuery validation doesn't work with the Range attribute and DateTime . For example, the following code will always display
a client side validation error, even when the date is in the specified range:
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
You will need to disable jQuery date validation to use the Range attribute with DateTime . It's generally not a good
practice to compile hard dates in your models, so using the Range attribute and DateTime is discouraged.
The following code shows combining attributes on one line:
public class Movie
{
public int Id { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}
In the next part of the series, we review the app and make some improvements to the automatically generated
Details and Delete methods.
Additional resources
Working with Forms
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
P R E V IO U S NEXT
Examine the Details and Delete methods of an
ASP.NET Core app
8/7/2019 • 3 minutes to read • Edit Online
By Rick Anderson
Open the Movie controller and examine the Details method:
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that
invokes the method. In this case it's a GET request with three URL segments, the Movies controller, the Details
method, and an id value. Recall these segments are defined in Startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
EF makes it easy to search for data using the FirstOrDefaultAsync method. An important security feature built into
the method is that the code verifies that the search method has found a movie before it tries to do anything with it.
For example, a hacker could introduce errors into the site by changing the URL created by the links from
http://localhost:{PORT}/Movies/Details/1 to something like http://localhost:{PORT}/Movies/Details/12345 (or
some other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw
an exception.
Examine the Delete and DeleteConfirmed methods.
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
Note that the HTTP GET Delete method doesn't delete the specified movie, it returns a view of the movie where you
can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter,
performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.
The [HttpPost] method that deletes the data is named DeleteConfirmed to give the HTTP POST method a unique
signature or name. The two method signatures are shown below:
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
The common language runtime (CLR ) requires overloaded methods to have a unique parameter signature (same
method name but different list of parameters). However, here you need two Delete methods -- one for GET and
one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a
parameter.)
There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding
mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a
URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that
method. The solution is what you see in the example, which is to add the ActionName("Delete") attribute to the
DeleteConfirmed method. That attribute performs mapping for the routing system so that a URL that includes
/Delete/ for a POST request will find the DeleteConfirmed method.
Another common work around for methods that have identical names and signatures is to artificially change the
signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when
we added the notUsed parameter. You could do the same thing here for the [HttpPost] Delete method:
// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)
Publish to Azure
For information on deploying to Azure, see Tutorial: Build a .NET Core and SQL Database web app in Azure App
Service.
P R E V IO U S
Build your first Blazor app
8/13/2019 • 8 minutes to read • Edit Online
Build components
1. Browse to each of the app's three pages in the Pages folder: Home, Counter, and Fetch data. These pages are
implemented by the Razor component files Index.razor, Counter.razor, and FetchData.razor.
2. On the Counter page, select the Click me button to increment the counter without a page refresh.
Incrementing a counter in a webpage normally requires writing JavaScript, but Blazor provides a better
approach using C#.
3. Examine the implementation of the Counter component in the Counter.razor file.
Pages/Counter.razor:
@page "/counter"
<h1>Counter</h1>
@code {
private int currentCount = 0;
The UI of the Counter component is defined using HTML. Dynamic rendering logic (for example, loops,
conditionals, expressions) is added using an embedded C# syntax called Razor. The HTML markup and C#
rendering logic are converted into a component class at build time. The name of the generated .NET class
matches the file name.
Members of the component class are defined in an @code block. In the @code block, component state
(properties, fields) and methods are specified for event handling or for defining other component logic.
These members are then used as part of the component's rendering logic and for handling events.
When the Click me button is selected:
The Counter component's registered onclick handler is called (the IncrementCount method).
The Counter component regenerates its render tree.
The new render tree is compared to the previous one.
Only modifications to the Document Object Model (DOM ) are applied. The displayed count is updated.
4. Modify the C# logic of the Counter component to make the count increment by two instead of one.
@page "/counter"
<h1>Counter</h1>
@code {
private int currentCount = 0;
5. Rebuild and run the app to see the changes. Select the Click me button. The counter increments by two.
Use components
Include a component in another component using an HTML syntax.
1. Add the Counter component to the app's Index component by adding a <Counter /> element to the
Index component ( Index.razor).
If you're using Blazor client-side for this experience, a SurveyPrompt component is used by the Index
component. Replace the <SurveyPrompt> element with a <Counter /> element. If you're using a Blazor
server-side app for this experience, add the <Counter /> element to the Index component:
Pages/Index.razor:
@page "/"
<h1>Hello, world!</h1>
<Counter />
2. Rebuild and run the app. The Index component has its own counter.
Component parameters
Components can also have parameters. Component parameters are defined using public properties on the
component class decorated with [Parameter] . Use attributes to specify arguments for a component in markup.
1. Update the component's @code C# code:
Add a IncrementAmount property decorated with the [Parameter] attribute.
Change the IncrementCount method to use the IncrementAmount when increasing the value of
currentCount .
Pages/Counter.razor:
@page "/counter"
<h1>Counter</h1>
@code {
private int currentCount = 0;
[Parameter]
public int IncrementAmount { get; set; } = 1;
1. Specify an IncrementAmount parameter in the Index component's <Counter> element using an attribute. Set
the value to increment the counter by ten.
Pages/Index.razor:
@page "/"
<h1>Hello, world!</h1>
2. Reload the Index component. The counter increments by ten each time the Click me button is selected. The
counter in the Counter component continues to increment by one.
Route to components
The @page directive at the top of the Counter.razor file specifies that the Counter component is a routing endpoint.
The Counter component handles requests sent to /counter . Without the @page directive, a component doesn't
handle routed requests, but the component can still be used by other components.
Dependency injection
Services registered in the app's service container are available to components via dependency injection (DI). Inject
services into a component using the @inject directive.
Examine the directives of the FetchData component.
If working with a Blazor server-side app, the WeatherForecastService service is registered as a singleton, so one
instance of the service is available throughout the app. The @inject directive is used to inject the instance of the
WeatherForecastService service into the component.
Pages/FetchData.razor:
@page "/fetchdata"
@using ToDoList.App.Services
@inject WeatherForecastService ForecastService
The FetchData component uses the injected service, as ForecastService , to retrieve an array of WeatherForecast
objects:
@code {
private WeatherForecast[] forecasts;
If working with a Blazor client-side app, HttpClient is injected to obtain weather forecast data from the
weather.json file in the wwwroot/sample-data folder:
Pages/FetchData.razor:
...
A @foreach loop is used to render each forecast instance as a row in the table of weather data:
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
<h1>Todo</h1>
4. Rebuild and run the app. Visit the new Todo page to confirm that the link to the Todo component works.
5. Add a TodoItem.cs file to the root of the project to hold a class that represents a todo item. Use the following
C# code for the TodoItem class:
@page "/todo"
<h1>Todo</h1>
<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>
@code {
private IList<TodoItem> todos = new List<TodoItem>();
}
7. The app requires UI elements for adding todo items to the list. Add a text input ( <input> ) and a button (
<button> ) below the unordered list ( <ul>...</ul> ):
@page "/todo"
<h1>Todo</h1>
<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>
@code {
private IList<TodoItem> todos = new List<TodoItem>();
}
8. Rebuild and run the app. When the Add todo button is selected, nothing happens because an event handler
isn't wired up to the button.
9. Add an AddTodo method to the Todo component and register it for button selections using the @onclick
attribute. The AddTodo C# method is called when the button is selected:
@code {
private IList<TodoItem> todos = new List<TodoItem>();
10. To get the title of the new todo item, add a newTodo string field at the top of the @code block and bind it to
the value of the text input using the bind attribute in the <input> element:
11. Update the AddTodo method to add the TodoItem with the specified title to the list. Clear the value of the
text input by setting newTodo to an empty string:
@page "/todo"
<h1>Todo</h1>
<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>
@code {
private IList<TodoItem> todos = new List<TodoItem>();
private string newTodo;
12. Rebuild and run the app. Add some todo items to the todo list to test the new code.
13. The title text for each todo item can be made editable, and a check box can help the user keep track of
completed items. Add a check box input for each todo item and bind its value to the IsDone property.
Change @todo.Title to an <input> element bound to @todo.Title :
<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="@todo.IsDone" />
<input @bind="@todo.Title" />
</li>
}
</ul>
14. To verify that these values are bound, update the <h1> header to show a count of the number of todo items
that aren't complete ( IsDone is false ).
<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="@todo.IsDone" />
<input @bind="@todo.Title" />
</li>
}
</ul>
@code {
private IList<TodoItem> todos = new List<TodoItem>();
private string newTodo;
16. Rebuild and run the app. Add todo items to test the new code.
Create and use ASP.NET Core Razor components
Tutorial: Create a web API with ASP.NET Core
8/9/2019 • 28 minutes to read • Edit Online
Overview
This tutorial creates the following API:
GET /api/TodoItems Get all to-do items None Array of to-do items
[
{
"date": "2019-07-16T19:04:05.7257911-06:00",
"temperatureC": 52,
"temperatureF": 125,
"summary": "Mild"
},
{
"date": "2019-07-17T19:04:05.7258461-06:00",
"temperatureC": 36,
"temperatureF": 96,
"summary": "Warm"
},
{
"date": "2019-07-18T19:04:05.7258467-06:00",
"temperatureC": 39,
"temperatureF": 102,
"summary": "Cool"
},
{
"date": "2019-07-19T19:04:05.7258471-06:00",
"temperatureC": 10,
"temperatureF": 49,
"summary": "Bracing"
},
{
"date": "2019-07-20T19:04:05.7258474-06:00",
"temperatureC": -1,
"temperatureF": 31,
"summary": "Chilly"
}
]
Visual Studio
Visual Studio Code
Visual Studio for Mac
In Solution Explorer, right-click the project. Select Add > New Folder. Name the folder Models.
Right-click the Models folder and select Add > Class. Name the class TodoItem and select Add.
Replace the template code with the following code:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
namespace TodoApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Scaffold a controller
Visual Studio
Visual Studio Code / Visual Studio for Mac
Right-click the Controllers folder.
Select Add > New Scaffolded Item.
Select API Controller with actions, using Entity Framework, and then select Add.
In the Add API Controller with actions, using Entity Framework dialog:
Select TodoItem (TodoAPI.Models) in the Model class.
Select TodoContext (TodoAPI.Models) in the Data context class.
Select Add
The generated code:
Defines an API controller class without methods.
Decorates the class with the [ApiController] attribute. This attribute indicates that the controller responds to web
API requests. For information about specific behaviors that the attribute enables, see Create web APIs with
ASP.NET Core.
Uses DI to inject the database context ( TodoContext ) into the controller. The database context is used in each of
the CRUD methods in the controller.
// POST: api/TodoItems
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. The method gets the value
of the to-do item from the body of the HTTP request.
The CreatedAtAction method:
Returns an HTTP 201 status code if successful. HTTP 201 is the standard response for an HTTP POST method
that creates a new resource on the server.
Adds a Location header to the response. The Location header specifies the URI of the newly created to-do item.
For more information, see 10.2.2 201 Created.
References the GetTodoItem action to create the Location header's URI. The C# nameof keyword is used to
avoid hard-coding the action name in the CreatedAtAction call.
Install Postman
This tutorial uses Postman to test the web API.
Install Postman
Start the web app.
Start Postman.
Disable SSL certificate verification
From File > Settings (*General tab), disable SSL certificate verification.
WARNING
Re-enable SSL certificate verification after testing the controller.
{
"name":"walk dog",
"isComplete":true
}
Select Send.
Test the app by calling the two endpoints from a browser or Postman. For example:
https://localhost:5001/api/TodoItems
https://localhost:5001/api/TodoItems/1
A response similar to the following is produced by the call to GetTodoItems :
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
private readonly TodoContext _context;
Replace [controller] with the name of the controller, which by convention is the controller class name
minus the "Controller" suffix. For this sample, the controller class name is TodoItemsController, so the
controller name is "TodoItems". ASP.NET Core routing is case insensitive.
If the [HttpGet] attribute has a route template (for example, [HttpGet("products")] ), append that to the
path. This sample doesn't use a template. For more information, see Attribute routing with Http[Verb]
attributes.
In the following GetTodoItem method, "{id}" is a placeholder variable for the unique identifier of the to-do item.
When GetTodoItem is invoked, the value of "{id}" in the URL is provided to the method in its id parameter.
// GET: api/TodoItems/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return todoItem;
}
Return values
The return type of the GetTodoItems and GetTodoItem methods is ActionResult<T> type. ASP.NET Core
automatically serializes the object to JSON and writes the JSON into the body of the response message. The
response code for this return type is 200, assuming there are no unhandled exceptions. Unhandled exceptions are
translated into 5xx errors.
ActionResult return types can represent a wide range of HTTP status codes. For example, GetTodoItem can return
two different status values:
If no item matches the requested ID, the method returns a 404 NotFound error code.
Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200
response.
// PUT: api/TodoItems/5
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id)
{
return BadRequest();
}
_context.Entry(todoItem).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TodoItemExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
PutTodoItem is similar to PostTodoItem , except it uses HTTP PUT. The response is 204 (No Content). According to
the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To
support partial updates, use HTTP PATCH.
If you get an error calling PutTodoItem , call GET to ensure there's an item in the database.
Test the PutTodoItem method
This sample uses an in-memory database that must be initialed each time the app is started. There must be an item
in the database before you make a PUT call. Call GET to insure there's an item in the database before making a PUT
call.
Update the to-do item that has ID = 1 and set its name to "feed fish":
{
"ID":1,
"name":"feed fish",
"isComplete":true
}
// DELETE: api/TodoItems/5
[HttpDelete("{id}")]
public async Task<ActionResult<TodoItem>> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return todoItem;
}
Overview
This tutorial creates the following API:
GET /api/TodoItems Get all to-do items None Array of to-do items
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later
WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work with
Visual Studio.
If you get a dialog box that asks if you should trust the IIS Express certificate, select Yes. In the Security Warning
dialog that appears next, select Yes.
The following JSON is returned:
["value1","value2"]
Visual Studio
Visual Studio Code
Visual Studio for Mac
In Solution Explorer, right-click the project. Select Add > New Folder. Name the folder Models.
Right-click the Models folder and select Add > Class. Name the class TodoItem and select Add.
Replace the template code with the following code:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}
namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
namespace TodoApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the
//container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP
//request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for
// production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
Add a controller
Visual Studio
Visual Studio Code / Visual Studio for Mac
Right-click the Controllers folder.
Select Add > New Item.
In the Add New Item dialog, select the API Controller Class template.
Name the class TodoController, and select Add.
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
// GET: api/Todo
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
return await _context.TodoItems.ToListAsync();
}
// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return todoItem;
}
Stop the app if it's still running. Then run it again to include the latest changes.
Test the app by calling the two endpoints from a browser. For example:
https://localhost:<port>/api/todo
https://localhost:<port>/api/todo/1
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
Routing and URL paths
The [HttpGet] attribute denotes a method that responds to an HTTP GET request. The URL path for each method
is constructed as follows:
Start with the template string in the controller's Route attribute:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
Replace [controller] with the name of the controller, which by convention is the controller class name
minus the "Controller" suffix. For this sample, the controller class name is TodoController, so the controller
name is "todo". ASP.NET Core routing is case insensitive.
If the [HttpGet] attribute has a route template (for example, [HttpGet("products")] ), append that to the
path. This sample doesn't use a template. For more information, see Attribute routing with Http[Verb]
attributes.
In the following GetTodoItem method, "{id}" is a placeholder variable for the unique identifier of the to-do item.
When GetTodoItem is invoked, the value of "{id}" in the URL is provided to the method in its id parameter.
// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return todoItem;
}
Return values
The return type of the GetTodoItems and GetTodoItem methods is ActionResult<T> type. ASP.NET Core
automatically serializes the object to JSON and writes the JSON into the body of the response message. The
response code for this return type is 200, assuming there are no unhandled exceptions. Unhandled exceptions are
translated into 5xx errors.
ActionResult return types can represent a wide range of HTTP status codes. For example, GetTodoItem can return
two different status values:
If no item matches the requested ID, the method returns a 404 NotFound error code.
Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200
response.
WARNING
Re-enable SSL certificate verification after testing the controller.
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. The method gets the value
of the to-do item from the body of the HTTP request.
The CreatedAtAction method:
Returns an HTTP 201 status code, if successful. HTTP 201 is the standard response for an HTTP POST
method that creates a new resource on the server.
Adds a Location header to the response. The Location header specifies the URI of the newly created to-do
item. For more information, see 10.2.2 201 Created.
References the GetTodoItem action to create the Location header's URI. The C# nameof keyword is used to
avoid hard-coding the action name in the CreatedAtAction call.
// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return todoItem;
}
{
"name":"walk dog",
"isComplete":true
}
Select Send.
If you get a 405 Method Not Allowed error, it's probably the result of not compiling the project after adding
the PostTodoItem method.
Test the location header URI
Select the Headers tab in the Response pane.
Copy the Location header value:
// PUT: api/Todo/5
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem item)
{
if (id != item.Id)
{
return BadRequest();
}
_context.Entry(item).State = EntityState.Modified;
await _context.SaveChangesAsync();
return NoContent();
}
PutTodoItem is similar to PostTodoItem , except it uses HTTP PUT. The response is 204 (No Content). According to
the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To
support partial updates, use HTTP PATCH.
If you get an error calling PutTodoItem , call GET to ensure there's an item in the database.
Test the PutTodoItem method
This sample uses an in-memory database that must be initialed each time the app is started. There must be an item
in the database before you make a PUT call. Call GET to insure there's an item in the database before making a PUT
call.
Update the to-do item that has id = 1 and set its name to "feed fish":
{
"ID":1,
"name":"feed fish",
"isComplete":true
}
// DELETE: api/Todo/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return NoContent();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc();
}
Create a wwwroot folder in the project directory.
Add an HTML file named index.html to the wwwroot directory. Replace its contents with the following markup:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #0066CC;
color: white;
}
td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Save">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Add a JavaScript file named site.js to the wwwroot directory. Replace its contents with the following code:
$(document).ready(function() {
getData();
});
function getData() {
$.ajax({
type: "GET",
url: uri,
cache: false,
success: function(data) {
const tBody = $("#todos");
$(tBody).empty();
getCount(data.length);
tr.appendTo(tBody);
});
});
todos = data;
}
});
}
function addItem() {
const item = {
name: $("#add-name").val(),
isComplete: false
};
$.ajax({
type: "POST",
accepts: "application/json",
url: uri,
contentType: "application/json",
data: JSON.stringify(item),
error: function(jqXHR, textStatus, errorThrown) {
alert("Something went wrong!");
},
success: function(result) {
getData();
$("#add-name").val("");
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + "/" + id,
type: "DELETE",
success: function(result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function(key, item) {
if (item.id === id) {
$("#edit-name").val(item.name);
$("#edit-id").val(item.id);
$("#edit-isComplete")[0].checked = item.isComplete;
}
});
$("#spoiler").css({ display: "block" });
}
$(".my-form").on("submit", function() {
const item = {
name: $("#edit-name").val(),
isComplete: $("#edit-isComplete").is(":checked"),
id: $("#edit-id").val()
};
$.ajax({
url: uri + "/" + $("#edit-id").val(),
type: "PUT",
accepts: "application/json",
contentType: "application/json",
data: JSON.stringify(item),
success: function(result) {
getData();
}
});
closeInput();
return false;
return false;
});
function closeInput() {
$("#spoiler").css({ display: "none" });
}
A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally:
Open Properties\launchSettings.json.
Remove the launchUrl property to force the app to open at index.html—the project's default file.
There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN.
This sample calls all of the CRUD methods of the API. Following are explanations of the calls to the API.
Get a list of to -do items
The jQuery ajax function sends a GET request to the API, which returns JSON representing an array of to-do
items. The success callback function is invoked if the request succeeds. In the callback, the DOM is updated with
the to-do information.
$(document).ready(function() {
getData();
});
function getData() {
$.ajax({
type: "GET",
url: uri,
cache: false,
success: function(data) {
const tBody = $("#todos");
$(tBody).empty();
getCount(data.length);
tr.appendTo(tBody);
});
todos = data;
}
});
}
$.ajax({
type: "POST",
accepts: "application/json",
url: uri,
contentType: "application/json",
data: JSON.stringify(item),
error: function(jqXHR, textStatus, errorThrown) {
alert("Something went wrong!");
},
success: function(result) {
getData();
$("#add-name").val("");
}
});
}
$.ajax({
url: uri + "/" + $("#edit-id").val(),
type: "PUT",
accepts: "application/json",
contentType: "application/json",
data: JSON.stringify(item),
success: function(result) {
getData();
}
});
$.ajax({
url: uri + "/" + id,
type: "DELETE",
success: function(result) {
getData();
}
});
Additional resources
View or download sample code for this tutorial. See how to download.
For more information, see the following resources:
Create web APIs with ASP.NET Core
ASP.NET Core Web API help pages with Swagger / OpenAPI
<xref:data/ef-rp/index>
Routing to controller actions in ASP.NET Core
Controller action return types in ASP.NET Core Web API
Deploy ASP.NET Core apps to Azure App Service
Host and deploy ASP.NET Core
YouTube version of this tutorial
Create a web API with ASP.NET Core and MongoDB
7/11/2019 • 10 minutes to read • Edit Online
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
.NET Core SDK 2.2 or later
Visual Studio 2019 with the ASP.NET and web development workload
MongoDB
Configure MongoDB
If using Windows, MongoDB is installed at C:\Program Files\MongoDB by default. Add C:\Program
Files\MongoDB\Server\<version_number>\bin to the Path environment variable. This change enables MongoDB
access from anywhere on your development machine.
Use the mongo Shell in the following steps to create a database, make collections, and store documents. For more
information on mongo Shell commands, see Working with the mongo Shell.
1. Choose a directory on your development machine for storing the data. For example, C:\BooksData on
Windows. Create the directory if it doesn't exist. The mongo Shell doesn't create new directories.
2. Open a command shell. Run the following command to connect to MongoDB on default port 27017.
Remember to replace <data_directory_path> with the directory you chose in the previous step.
3. Open another command shell instance. Connect to the default test database by running the following
command:
mongo
4. Run the following in a command shell:
use BookstoreDb
If it doesn't already exist, a database named BookstoreDb is created. If the database does exist, its connection
is opened for transactions.
5. Create a Books collection using following command:
db.createCollection('Books')
{ "ok" : 1 }
6. Define a schema for the Books collection and insert two documents using the following command:
db.Books.insertMany([{'Name':'Design Patterns','Price':54.93,'Category':'Computers','Author':'Ralph
Johnson'}, {'Name':'Clean Code','Price':43.15,'Category':'Computers','Author':'Robert C. Martin'}])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5bfd996f7b8e48dc15ff215d"),
ObjectId("5bfd996f7b8e48dc15ff215e")
]
}
NOTE
The ID's shown in this article will not match the IDs when you run this sample.
db.Books.find({}).pretty()
The schema adds an autogenerated _id property of type ObjectId for each document.
The database is ready. You can start creating the ASP.NET Core web API.
namespace BooksApi.Models
{
public class Book
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
[BsonElement("Name")]
public string BookName { get; set; }
{
"BookstoreDatabaseSettings": {
"BooksCollectionName": "Books",
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BookstoreDb"
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}
2. Add a BookstoreDatabaseSettings.cs file to the Models directory with the following code:
namespace BooksApi.Models
{
public class BookstoreDatabaseSettings : IBookstoreDatabaseSettings
{
public string BooksCollectionName { get; set; }
public string ConnectionString { get; set; }
public string DatabaseName { get; set; }
}
services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
using BooksApi.Models;
namespace BooksApi.Services
{
public class BookService
{
private readonly IMongoCollection<Book> _books;
_books = database.GetCollection<Book>(settings.BooksCollectionName);
}
In the preceding code, an IBookstoreDatabaseSettings instance is retrieved from DI via constructor injection.
This technique provides access to the appsettings.json configuration values that were added in the Add a
configuration model section.
3. Add the following highlighted code to Startup.ConfigureServices :
services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);
services.AddSingleton<BookService>();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
In the preceding code, the BookService class is registered with DI to support constructor injection in
consuming classes. The singleton service lifetime is most appropriate because BookService takes a direct
dependency on MongoClient . Per the official Mongo Client reuse guidelines, MongoClient should be
registered in DI with a singleton service lifetime.
4. Add the following code to the top of Startup.cs to resolve the BookService reference:
using BooksApi.Services;
The BookService class uses the following MongoDB.Driver members to perform CRUD operations against the
database:
MongoClient – Reads the server instance for performing database operations. The constructor of this class is
provided the MongoDB connection string:
_books = database.GetCollection<Book>(settings.BooksCollectionName);
}
IMongoDatabase – Represents the Mongo database for performing operations. This tutorial uses the generic
GetCollection<TDocument>(collection) method on the interface to gain access to data in a specific
collection. Perform CRUD operations against the collection after this method is called. In the
GetCollection<TDocument>(collection) method call:
Add a controller
Add a BooksController class to the Controllers directory with the following code:
using BooksApi.Models;
using BooksApi.Services;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace BooksApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
private readonly BookService _bookService;
[HttpGet]
public ActionResult<List<Book>> Get() =>
_bookService.Get();
if (book == null)
{
return NotFound();
}
return book;
}
[HttpPost]
public ActionResult<Book> Create(Book book)
{
_bookService.Create(book);
[HttpPut("{id:length(24)}")]
public IActionResult Update(string id, Book bookIn)
{
var book = _bookService.Get(id);
if (book == null)
{
return NotFound();
}
_bookService.Update(id, bookIn);
return NoContent();
}
[HttpDelete("{id:length(24)}")]
public IActionResult Delete(string id)
{
var book = _bookService.Get(id);
if (book == null)
{
return NotFound();
}
_bookService.Remove(book.Id);
return NoContent();
}
}
}
[
{
"id":"5bfd996f7b8e48dc15ff215d",
"bookName":"Design Patterns",
"price":54.93,
"category":"Computers",
"author":"Ralph Johnson"
},
{
"id":"5bfd996f7b8e48dc15ff215e",
"bookName":"Clean Code",
"price":43.15,
"category":"Computers",
"author":"Robert C. Martin"
}
]
{
"id":"{ID}",
"bookName":"Clean Code",
"price":43.15,
"category":"Computers",
"author":"Robert C. Martin"
}
services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);
services.AddSingleton<BookService>();
services.AddMvc()
.AddJsonOptions(options => options.UseMemberCasing())
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
With the preceding change, property names in the web API's serialized JSON response match their
corresponding property names in the CLR object type. For example, the Book class's Author property
serializes as Author .
2. In Models/Book.cs, annotate the BookName property with the following [JsonProperty] attribute:
[BsonElement("Name")]
[JsonProperty("Name")]
public string BookName { get; set; }
The [JsonProperty] attribute's value of Name represents the property name in the web API's serialized
JSON response.
3. Add the following code to the top of Models/Book.cs to resolve the [JsonProperty] attribute reference:
using Newtonsoft.Json;
4. Repeat the steps defined in the Test the web API section. Notice the difference in JSON property names.
Next steps
For more information on building ASP.NET Core web APIs, see the following resources:
YouTube version of this article
Create web APIs with ASP.NET Core
Controller action return types in ASP.NET Core Web API
Tutorial: Call an ASP.NET Core web API with jQuery
7/29/2019 • 4 minutes to read • Edit Online
By Rick Anderson
This tutorial shows how to call an ASP.NET Core web API with jQuery
For ASP.NET Core 2.2, see the 2.2 version of Call the Web API with jQuery.
Prerequisites
Complete Tutorial: Create a web API
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
#spoiler {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #0066CC;
color: white;
}
td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Save">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.4.1.js"
integrity="sha384-mlceH9HlqLp7GMKHrj5Ara1+LvdTZVMx4S1U43/NxCvAkzIo8WJ0FE7duLel3wVo"
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Add a JavaScript file named site.js to the wwwroot directory. Replace its contents with the following code:
$(document).ready(function() {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
cache: false,
success: function(data) {
const tBody = $('#todos');
$(tBody).empty();
getCount(data.length);
tr.appendTo(tBody);
});
todos = data;
}
});
}
function addItem() {
const item = {
name: $('#add-name').val(),
isComplete: false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function(jqXHR, textStatus, errorThrown) {
alert('Something went wrong!');
},
success: function(result) {
getData();
$('#add-name').val('');
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function(result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function(key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete')[0].checked = item.isComplete;
}
});
$('#spoiler').css({ display: 'block' });
}
$('.my-form').on('submit', function() {
const item = {
name: $('#edit-name').val(),
isComplete: $('#edit-isComplete').is(':checked'),
id: parseInt($('#edit-id').val(), 10)
};
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function(result) {
getData();
}
});
closeInput();
return false;
});
function closeInput() {
$('#spoiler').css({ display: 'none' });
}
A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally:
Open Properties\launchSettings.json.
Remove the launchUrl property to force the app to open at index.html—the project's default file.
There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN.
This sample calls all of the CRUD methods of the API. Following are explanations of the calls to the API.
Get a list of to -do items
The jQuery ajax function sends a GET request to the API, which returns JSON representing an array of to-do
items. The success callback function is invoked if the request succeeds. In the callback, the DOM is updated with
the to-do information.
$(document).ready(function() {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
cache: false,
success: function(data) {
const tBody = $('#todos');
$(tBody).empty();
getCount(data.length);
tr.appendTo(tBody);
});
todos = data;
}
});
}
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function(jqXHR, textStatus, errorThrown) {
alert('Something went wrong!');
},
success: function(result) {
getData();
$('#add-name').val('');
}
});
}
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function(result) {
getData();
}
});
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function(result) {
getData();
}
});
Advance to the next tutorial to learn how to generate API help pages:
Get started with Swashbuckle and ASP.NET Core
Create backend services for native mobile apps with
ASP.NET Core
4/26/2019 • 8 minutes to read • Edit Online
By Steve Smith
Mobile apps can communicate with ASP.NET Core backend services. For instructions on connecting local web
services from iOS simulators and Android emulators, see Connect to Local Web Services from iOS Simulators and
Android Emulators.
View or download sample backend services code
NOTE
Make sure you run the application directly, rather than behind IIS Express, which ignores non-local requests by default. Run
dotnet run from a command prompt, or choose the application name profile from the Debug Target dropdown in the Visual
Studio toolbar.
Add a model class to represent To-Do items. Mark required fields using the [Required] attribute:
using System.ComponentModel.DataAnnotations;
namespace ToDoApi.Models
{
public class ToDoItem
{
[Required]
public string ID { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Notes { get; set; }
The API methods require some way to work with data. Use the same IToDoRepository interface the original
Xamarin sample uses:
using System.Collections.Generic;
using ToDoApi.Models;
namespace ToDoApi.Interfaces
{
public interface IToDoRepository
{
bool DoesItemExist(string id);
IEnumerable<ToDoItem> All { get; }
ToDoItem Find(string id);
void Insert(ToDoItem item);
void Update(ToDoItem item);
void Delete(string id);
}
}
For this sample, the implementation just uses a private collection of items:
using System.Collections.Generic;
using System.Linq;
using ToDoApi.Interfaces;
using ToDoApi.Models;
namespace ToDoApi.Services
{
public class ToDoRepository : IToDoRepository
{
private List<ToDoItem> _toDoList;
public ToDoRepository()
{
InitializeData();
}
_toDoList.Add(todoItem1);
_toDoList.Add(todoItem2);
_toDoList.Add(todoItem3);
}
}
}
services.AddSingleton<IToDoRepository,ToDoRepository>();
}
TIP
Learn more about creating web APIs in Build your first Web API with ASP.NET Core MVC and Visual Studio.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ToDoApi.Interfaces;
using ToDoApi.Models;
namespace ToDoApi.Controllers
{
[Route("api/[controller]")]
public class ToDoItemsController : Controller
{
private readonly IToDoRepository _toDoRepository;
This API supports four different HTTP verbs to perform CRUD (Create, Read, Update, Delete) operations on the
data source. The simplest of these is the Read operation, which corresponds to an HTTP GET request.
Reading Items
Requesting a list of items is done with a GET request to the List method. The [HttpGet] attribute on the List
method indicates that this action should only handle GET requests. The route for this action is the route specified
on the controller. You don't necessarily need to use the action name as part of the route. You just need to ensure
each action has a unique and unambiguous route. Routing attributes can be applied at both the controller and
method levels to build up specific routes.
[HttpGet]
public IActionResult List()
{
return Ok(_toDoRepository.All);
}
The List method returns a 200 OK response code and all of the ToDo items, serialized as JSON.
You can test your new API method using a variety of tools, such as Postman, shown here:
Creating Items
By convention, creating new data items is mapped to the HTTP POST verb. The Create method has an
[HttpPost] attribute applied to it, and accepts a ToDoItem instance. Since the item argument will be passed in the
body of the POST, this parameter is decorated with the [FromBody] attribute.
Inside the method, the item is checked for validity and prior existence in the data store, and if no issues occur, it's
added using the repository. Checking ModelState.IsValid performs model validation, and should be done in every
API method that accepts user input.
[HttpPost]
public IActionResult Create([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
bool itemExists = _toDoRepository.DoesItemExist(item.ID);
if (itemExists)
{
return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
}
_toDoRepository.Insert(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
}
return Ok(item);
}
The sample uses an enum containing error codes that are passed to the mobile client:
Test adding new items using Postman by choosing the POST verb providing the new object in JSON format in the
Body of the request. You should also add a request header specifying a Content-Type of application/json .
The method returns the newly created item in the response.
Updating Items
Modifying records is done using HTTP PUT requests. Other than this change, the Edit method is almost identical
to Create . Note that if the record isn't found, the Edit action will return a NotFound (404) response.
[HttpPut]
public IActionResult Edit([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
var existingItem = _toDoRepository.Find(item.ID);
if (existingItem == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Update(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
}
return NoContent();
}
To test with Postman, change the verb to PUT. Specify the updated object data in the Body of the request.
This method returns a NoContent (204) response when successful, for consistency with the pre-existing API.
Deleting Items
Deleting records is accomplished by making DELETE requests to the service, and passing the ID of the item to be
deleted. As with updates, requests for items that don't exist will receive NotFound responses. Otherwise, a
successful request will get a NoContent (204) response.
[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
try
{
var item = _toDoRepository.Find(id);
if (item == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Delete(id);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
}
return NoContent();
}
Note that when testing the delete functionality, nothing is required in the Body of the request.
Additional resources
Authentication and Authorization
Tutorial: Get started with ASP.NET Core SignalR
7/15/2019 • 13 minutes to read • Edit Online
This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:
Create a web project.
Add the SignalR client library.
Create a SignalR hub.
Configure the project to use SignalR.
Add code that sends messages from any client to all connected clients.
At the end, you'll have a working chat app:
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 3.0 Preview
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
The ChatHub class inherits from the SignalR Hub class. The Hub class manages connections, groups, and
messaging.
The SendMessage method can be called by a connected client to send a message to all clients. JavaScript
client code that calls the method is shown later in the tutorial. SignalR code is asynchronous to provide
maximum scalability.
Configure SignalR
The SignalR server must be configured to pass SignalR requests to SignalR.
Add the following highlighted code to the Startup.cs file.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a
given request.
options.CheckConsentNeeded = context => true;
});
services.AddRazorPages();
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request
pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production
scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/chatHub");
});
}
}
}
These changes add SignalR to the ASP.NET Core dependency injection and routing systems.
@page
<div class="container">
<div class="row"> </div>
<div class="row">
<div class="col-2">User</div>
<div class="col-4"><input type="text" id="userInput" /></div>
</div>
<div class="row">
<div class="col-2">Message</div>
<div class="col-4"><input type="text" id="messageInput" /></div>
</div>
<div class="row"> </div>
<div class="row">
<div class="col-6">
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
Next steps
To learn more about SignalR, see the introduction:
Introduction to ASP.NET Core SignalR
This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:
Create a web project.
Add the SignalR client library.
Create a SignalR hub.
Configure the project to use SignalR.
Add code that sends messages from any client to all connected clients.
At the end, you'll have a working chat app:
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2017 version 15.9 or later with the ASP.NET and web development workload. You can use
Visual Studio 2019, but some project creation steps differ from what's shown in the tutorial.
.NET Core SDK 2.2 or later
WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work with
Visual Studio.
Select Choose specific files, expand the dist/browser folder, and select signalr.js and signalr.min.js.
Set Target Location to wwwroot/lib/signalr/, and select Install.
LibMan creates a wwwroot/lib/signalr folder and copies the selected files to it.
Create a SignalR hub
A hub is a class that serves as a high-level pipeline that handles client-server communication.
In the SignalRChat project folder, create a Hubs folder.
In the Hubs folder, create a ChatHub.cs file with the following code:
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
The ChatHub class inherits from the SignalR Hub class. The Hub class manages connections, groups, and
messaging.
The SendMessage method can be called by a connected client to send a message to all clients. JavaScript
client code that calls the method is shown later in the tutorial. SignalR code is asynchronous to provide
maximum scalability.
Configure SignalR
The SignalR server must be configured to pass SignalR requests to SignalR.
Add the following highlighted code to the Startup.cs file.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SignalRChat.Hubs;
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a
given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request
pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub");
});
app.UseMvc();
}
}
}
These changes add SignalR to the ASP.NET Core dependency injection system and the middleware pipeline.
@page
<div class="container">
<div class="row"> </div>
<div class="row">
<div class="col-6"> </div>
<div class="col-6">
User..........<input type="text" id="userInput" />
<br />
Message...<input type="text" id="messageInput" />
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6"> </div>
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
connection.start().then(function(){
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
Next steps
In this tutorial, you learned how to:
Create a web app project.
Add the SignalR client library.
Create a SignalR hub.
Configure the project to use SignalR.
Add code that uses the hub to send messages from any client to all connected clients.
To learn more about SignalR, see the introduction:
Introduction to ASP.NET Core SignalR
Use ASP.NET Core SignalR with TypeScript and
Webpack
5/14/2019 • 10 minutes to read • Edit Online
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later
Node.js with npm
npm init -y
{
"name": "SignalRWebPack",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Setting the private property to true prevents package installation warnings in the next step.
3. Install the required npm packages. Execute the following command from the project root:
npm install -D -E [email protected] [email protected] [email protected] mini-css-
[email protected] [email protected] [email protected] [email protected] [email protected]
"scripts": {
"build": "webpack --mode=development --watch",
"release": "webpack --mode=production",
"publish": "npm run release && dotnet publish -c Release"
},
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "wwwroot"),
filename: "[name].[chunkhash].js",
publicPath: "/"
},
resolve: {
extensions: [".js", ".ts"]
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader"
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
},
plugins: [
new CleanWebpackPlugin(["wwwroot/*"]),
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new MiniCssExtractPlugin({
filename: "css/[name].[chunkhash].css"
})
]
};
The preceding file configures the Webpack compilation. Some configuration details to note:
The output property overrides the default value of dist. The bundle is instead emitted in the wwwroot
directory.
The resolve.extensions array includes .js to import the SignalR client JavaScript.
6. Create a new src directory in the project root. Its purpose is to store the project's client-side assets.
7. Create src/index.html with the following content.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ASP.NET Core SignalR</title>
</head>
<body>
<div id="divMessages" class="messages">
</div>
<div class="input-zone">
<label id="lblMessage" for="tbMessage">Message:</label>
<input id="tbMessage" class="input-zone-input" type="text" />
<button id="btnSend">Send</button>
</div>
</body>
</html>
The preceding HTML defines the homepage's boilerplate markup.
8. Create a new src/css directory. Its purpose is to store the project's .css files.
9. Create src/css/main.css with the following content:
*, *::before, *::after {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
}
.input-zone {
align-items: center;
display: flex;
flex-direction: row;
margin: 10px;
}
.input-zone-input {
flex: 1;
margin-right: 10px;
}
.message-author {
font-weight: bold;
}
.messages {
border: 1px solid #000;
margin: 10px;
max-height: 300px;
min-height: 300px;
overflow-y: auto;
padding: 5px;
}
{
"compilerOptions": {
"target": "es5"
}
}
The preceding code configures the TypeScript compiler to produce ECMAScript 5-compatible JavaScript.
11. Create src/index.ts with the following content:
import "./css/main.css";
btnSend.addEventListener("click", send);
function send() {
}
The preceding TypeScript retrieves references to DOM elements and attaches two event handlers:
keyup : This event fires when the user types something in the textbox identified as tbMessage . The send
function is called when the user presses the Enter key.
click : This event fires when the user clicks the Send button. The send function is called.
app.UseDefaultFiles();
app.UseStaticFiles();
The preceding code allows the server to locate and serve the index.html file, whether the user enters its full
URL or the root URL of the web app.
2. Call AddSignalR in the Startup.ConfigureServices method. It adds the SignalR services to your project.
services.AddSignalR();
3. Map a /hub route to the ChatHub hub. Add the following lines at the end of the Startup.Configure method:
app.UseSignalR(options =>
{
options.MapHub<ChatHub>("/hub");
});
4. Create a new directory, called Hubs, in the project root. Its purpose is to store the SignalR hub, which is
created in the next step.
5. Create hub Hubs/ChatHub.cs with the following code:
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
}
}
6. Add the following code at the top of the Startup.cs file to resolve the ChatHub reference:
using SignalRWebPack.Hubs;
The preceding command installs the SignalR TypeScript client, which allows the client to send messages to
the server.
2. Add the highlighted code to the src/index.ts file:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";
m.innerHTML =
`<div class="message-author">${username}</div><div>${message}</div>`;
divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});
btnSend.addEventListener("click", send);
function send() {
}
The preceding code supports receiving messages from the server. The HubConnectionBuilder class creates a
new builder for configuring the server connection. The withUrl function configures the hub URL.
SignalR enables the exchange of messages between a client and a server. Each message has a specific name.
For example, you can have messages with the name messageReceived that execute the logic responsible for
displaying the new message in the messages zone. Listening to a specific message can be done via the on
function. You can listen to any number of message names. It's also possible to pass parameters to the
message, such as the author's name and the content of the message received. Once the client receives a
message, a new div element is created with the author's name and the message content in its innerHTML
attribute. It's added to the main div element displaying the messages.
3. Now that the client can receive a message, configure it to send messages. Add the highlighted code to the
src/index.ts file:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";
messageContainer.innerHTML =
`<div class="message-author">${username}</div><div>${message}</div>`;
divMessages.appendChild(messageContainer);
divMessages.scrollTop = divMessages.scrollHeight;
});
btnSend.addEventListener("click", send);
function send() {
connection.send("newMessage", username, tbMessage.value)
.then(() => tbMessage.value = "");
}
Sending a message through the WebSockets connection requires calling the send method. The method's
first parameter is the message name. The message data inhabits the other parameters. In this example, a
message identified as newMessage is sent to the server. The message consists of the username and the user
input from a text box. If the send works, the text box value is cleared.
4. Add the highlighted method to the ChatHub class:
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
public async Task NewMessage(long username, string message)
{
await Clients.All.SendAsync("messageReceived", username, message);
}
}
}
The preceding code broadcasts received messages to all connected users once the server receives them. It's
unnecessary to have a generic on method to receive all the messages. A method named after the message
name suffices.
In this example, the TypeScript client sends a message identified as newMessage . The C# NewMessage method
expects the data sent by the client. A call is made to the SendAsync method on Clients.All. The received
messages are sent to all clients connected to the hub.
This command yields the client-side assets to be served when running the app. The assets are placed in the
wwwroot folder.
Webpack completed the following tasks:
Purged the contents of the wwwroot directory.
Converted the TypeScript to JavaScript—a process known as transpilation.
Mangled the generated JavaScript to reduce file size—a process known as minification.
Copied the processed JavaScript, CSS, and HTML files from src to the wwwroot directory.
Injected the following elements into the wwwroot/index.html file:
A <link> tag, referencing the wwwroot/main.<hash>.css file. This tag is placed immediately
before the closing </head> tag.
A <script> tag, referencing the minified wwwroot/main.<hash>.js file. This tag is placed
immediately before the closing </body> tag.
2. Select Debug > Start without debugging to launch the app in a browser without attaching the debugger.
The wwwroot/index.html file is served at http://localhost:<port_number> .
3. Open another browser instance (any browser). Paste the URL in the address bar.
4. Choose either browser, type something in the Message text box, and click the Send button. The unique user
name and message are displayed on both pages instantly.
Additional resources
ASP.NET Core SignalR JavaScript client
Use hubs in ASP.NET Core SignalR
Tutorial: Create a gRPC client and server in ASP.NET
Core
8/7/2019 • 7 minutes to read • Edit Online
By John Luo
This tutorial shows how to create a .NET Core gRPC client and an ASP.NET Core gRPC Server.
At the end, you'll have a gRPC client that communicates with the gRPC Greeter service.
View or download sample code (how to download).
In this tutorial, you:
Create a gRPC Server.
Create a gRPC client.
Test the gRPC client service with the gRPC Greeter service.
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 3.0 Preview
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
NOTE
The gRPC template is configured to use Transport Layer Security (TLS). gRPC clients need to use HTTPS to call the server.
macOS doesn't support ASP.NET Core gRPC with TLS. Additional configuration is required to successfully run gRPC services
on macOS. For more information, see gRPC and ASP.NET Core on macOS.
Visual Studio
Visual Studio Code
Visual Studio for Mac
Install the packages using either the Package Manager Console (PMC ) or Manage NuGet Packages.
PMC option to install packages
From Visual Studio, select Tools > NuGet Package Manager > Package Manager Console
From the Package Manager Console window, navigate to the directory in which the
GrpcGreeterClient.csproj file exists.
Run the following commands:
Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools
Add an item group with a <Protobuf> element that refers to the greet.proto file:
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
namespace GrpcGreeterClient
{
class Program
{
static async Task Main(string[] args)
{
var httpClient = new HttpClient();
// The port number(5001) must match the port of the gRPC server.
httpClient.BaseAddress = new Uri("https://localhost:5001");
var client = GrpcClient.Create<Greeter.GreeterClient>(httpClient);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}
Program.cs contains the entry point and logic for the gRPC client.
The Greeter client is created by:
Instantiating an HttpClient containing the information for creating the connection to the gRPC service.
Using the HttpClient to construct the Greeter client:
The Greeter client calls the asynchronous SayHello method. The result of the SayHello call is displayed:
The client sends a greeting to the service with a message containing its name "GreeterClient". The service sends
the message "Hello GreeterClient" as a response. The "Hello GreeterClient" response is displayed in the command
prompt:
The gRPC service records the details of the successful call in the logs written to the command prompt.
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\sample\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/2 POST https://localhost:5001/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 78.32260000000001ms 200 application/grpc
Next steps
Introduction to gRPC on ASP.NET Core
gRPC services with C#
Migrating gRPC services from C -core to ASP.NET Core
Razor Pages with Entity Framework Core in ASP.NET
Core - Tutorial 1 of 8
8/9/2019 • 32 minutes to read • Edit Online
Prerequisites
If you're new to Razor Pages, go through the Get started with Razor Pages tutorial series before starting this
one.
Visual Studio
Visual Studio Code
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 3.0 Preview
Database engines
The Visual Studio instructions use SQL Server LocalDB, a version of SQL Server Express that runs only on
Windows.
The Visual Studio Code instructions use SQLite, a cross-platform database engine.
If you choose to use SQLite, download and install a third-party tool for managing and viewing a SQLite database,
such as DB Browser for SQLite.
Troubleshooting
If you run into a problem you can't resolve, compare your code to the completed project. A good way to get help is
by posting a question to StackOverflow.com, using the ASP.NET Core tag or the EF Core tag.
Update-Database
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
In Pages/Index.cshtml, replace the contents of the file with the following code to replace the text about ASP.NET
Core with text about this app:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
using System;
using System.Collections.Generic;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
The ID property becomes the primary key column of the database table that corresponds to this class. By default,
EF Core interprets a property that's named ID or classnameID as the primary key. So the alternative automatically
recognized name for the Student class primary key is StudentID .
The Enrollments property is a navigation property. Navigation properties hold other entities that are related to this
entity. In this case, the Enrollments property of a Student entity holds all of the Enrollment entities that are
related to that Student. For example, if a Student row in the database has two related Enrollment rows, the
Enrollments navigation property contains those two Enrollment entities.
In the database, an Enrollment row is related to a Student row if its StudentID column contains the student's ID
value. For example, suppose a Student row has ID=1. Related Enrollment rows will have StudentID = 1. StudentID
is a foreign key in the Enrollment table.
The Enrollments property is defined as ICollection<Enrollment> because there may be multiple related
Enrollment entities. You can use other collection types, such as List<Enrollment> or HashSet<Enrollment> . When
ICollection<Enrollment> is used, EF Core creates a HashSet<Enrollment> collection by default.
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
The EnrollmentID property is the primary key; this entity uses the classnameID pattern instead of ID by itself. For
a production data model, choose one pattern and use it consistently. This tutorial uses both just to illustrate that
both work. Using ID without classname makes it easier to implement some kinds of data model changes.
The Grade property is an enum . The question mark after the Grade type declaration indicates that the Grade
property is nullable. A grade that's null is different from a zero grade—null means a grade isn't known or hasn't
been assigned yet.
The StudentID property is a foreign key, and the corresponding navigation property is Student . An Enrollment
entity is associated with one Student entity, so the property contains a single Student entity.
The CourseID property is a foreign key, and the corresponding navigation property is Course . An Enrollment
entity is associated with one Course entity.
EF Core interprets a property as a foreign key if it's named <navigation property name><primary key property name> .
For example, StudentID is the foreign key for the Student navigation property, since the Student entity's primary
key is ID . Foreign key properties can also be named <primary key property name> . For example, CourseID since
the Course entity's primary key is CourseID .
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
The Enrollments property is a navigation property. A Course entity can be related to any number of Enrollment
entities.
The DatabaseGenerated attribute allows the app to specify the primary key rather than having the database
generate it.
Build the project to validate that there are no compiler errors.
Visual Studio
Visual Studio Code
Create a Students folder in the Pages folder.
In Solution Explorer, right-click the Pages/Students folder and select Add > New Scaffolded Item.
In the Add Scaffold dialog, select Razor Pages using Entity Framework (CRUD ) > ADD.
In the Add Razor Pages using Entity Framework (CRUD ) dialog:
In the Model class drop-down, select Student (ContosoUniversity.Models).
In the Data context class row, select the + (plus) sign.
Change the data context name from ContosoUniversity.Models.ContosoUniversityContext to
ContosoUniversity.Data.SchoolContext.
Select Add.
The following packages are automatically installed:
Microsoft.VisualStudio.Web.CodeGeneration.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.Extensions.Logging.Debug
Microsoft.EntityFrameworkCore.Tools
If you have a problem with the preceding step, build the project and retry the scaffold step.
The scaffolding process:
Creates Razor pages in the Pages/Students folder:
Create.cshtml and Create.cshtml.cs
Delete.cshtml and Delete.cshtml.cs
Details.cshtml and Details.cshtml.cs
Edit.cshtml and Edit.cshtml.cs
Index.cshtml and Index.cshtml.cs
Creates Data/SchoolContext.cs.
Adds the context to dependency injection in Startup.cs.
Adds a database connection string to appsettings.json.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SchoolContext": "Server=
(localdb)\\mssqllocaldb;Database=SchoolContext;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
LocalDB is a lightweight version of the SQL Server Express Database Engine and is intended for app development,
not production use. By default, LocalDB creates .mdf files in the C:/Users/<user> directory.
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext (DbContextOptions<SchoolContext> options)
: base(options)
{
}
The highlighted code creates a DbSet<TEntity> property for each entity set. In EF Core terminology:
An entity set typically corresponds to a database table.
An entity corresponds to a row in the table.
Since an entity set contains multiple entities, the DBSet properties should be plural names. Since the scaffolding
tool created a Student DBSet, this step changes it to plural Students .
To make the Razor Pages code match the new DBSet name, make a global change across the whole project of
_context.Student to _context.Students . There are 8 occurrences.
Startup.cs
ASP.NET Core is built with dependency injection. Services (such as the EF Core database context) are registered
with dependency injection during application startup. Components that require these services (such as Razor
Pages) are provided these services via constructor parameters. The constructor code that gets a database context
instance is shown later in the tutorial.
The scaffolding tool automatically registered the context class with the dependency injection container.
Visual Studio
Visual Studio Code
In ConfigureServices , the highlighted lines were added by the scaffolder:
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));
}
The name of the connection string is passed in to the context by calling a method on a DbContextOptions object.
For local development, the ASP.NET Core configuration system reads the connection string from the
appsettings.json file.
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
CreateDbIfNotExists(host);
host.Run();
}
try
{
var context = services.GetRequiredService<SchoolContext>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
}
The EnsureCreated method takes no action if a database for the context exists. If no database exists, it creates the
database and schema. EnsureCreated enables the following workflow for handling data model changes:
Delete the database. Any existing data is lost.
Change the data model. For example, add an EmailAddress field.
Run the app.
EnsureCreated creates a database with the new schema.
This workflow works well early in development when the schema is rapidly evolving, as long as you don't need to
preserve data. The situation is different when data that has been entered into the database needs to be preserved.
When that is the case, use migrations.
Later in the tutorial series, you delete the database that was created by EnsureCreated and use migrations instead.
A database that is created by EnsureCreated can't be updated by using migrations.
Test the app
Run the app.
Select the Students link and then Create New.
Test the Edit, Details, and Delete links.
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using System;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();
The code checks if there are any students in the database. If there are no students, it adds test data to the database.
It creates the test data in arrays rather than List<T> collections to optimize performance.
In Program.cs, replace the EnsureCreated call with a DbInitializer.Initialize call:
// context.Database.EnsureCreated();
DbInitializer.Initialize(context);
Visual Studio
Visual Studio Code
Run the app, delete any student records you created earlier, and stop the app.
Restart the app.
Select the Students page to see the seeded data.
Asynchronous code
Asynchronous programming is the default mode for ASP.NET Core and EF Core.
A web server has a limited number of threads available, and in high load situations all of the available threads
might be in use. When that happens, the server can't process new requests until the threads are freed up. With
synchronous code, many threads may be tied up while they aren't actually doing any work because they're waiting
for I/O to complete. With asynchronous code, when a process is waiting for I/O to complete, its thread is freed up
for the server to use for processing other requests. As a result, asynchronous code enables server resources to be
used more efficiently, and the server can handle more traffic without delays.
Asynchronous code does introduce a small amount of overhead at run time. For low traffic situations, the
performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.
In the following code, the async keyword, Task<T> return value, await keyword, and ToListAsync method make
the code execute asynchronously.
Some things to be aware of when writing asynchronous code that uses EF Core:
Only statements that cause queries or commands to be sent to the database are executed asynchronously. That
includes ToListAsync , SingleOrDefaultAsync , FirstOrDefaultAsync , and SaveChangesAsync . It doesn't include
statements that just change an IQueryable , such as
var students = context.Students.Where(s => s.LastName == "Davolio") .
An EF Core context isn't thread safe: don't try to do multiple operations in parallel.
To take advantage of the performance benefits of async code, verify that library packages (such as for paging)
use async if they call EF Core methods that send queries to the database.
For more information about asynchronous programming in .NET, see Async Overview and Asynchronous
programming with async and await.
Next steps
NEXT
T U T O R IA L
The Contoso University sample web app demonstrates how to create an ASP.NET Core Razor Pages app using
Entity Framework (EF ) Core.
The sample app is a web site for a fictional Contoso University. It includes functionality such as student admission,
course creation, and instructor assignments. This page is the first in a series of tutorials that explain how to build
the Contoso University sample app.
Download or view the completed app. Download instructions.
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio 2019 with the following workloads:
ASP.NET and web development
.NET Core cross-platform development
.NET Core 2.1 SDK or later
Familiarity with Razor Pages. New programmers should complete Get started with Razor Pages before starting this
series.
Troubleshooting
If you run into a problem you can't resolve, you can generally find the solution by comparing your code to the
completed project. A good way to get help is by posting a question to StackOverflow.com for ASP.NET Core or EF
Core.
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">Contoso University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Students/Index">Students</a></li>
<li><a asp-page="/Courses/Index">Courses</a></li>
<li><a asp-page="/Instructors/Index">Instructors</a></li>
<li><a asp-page="/Departments/Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
In Pages/Index.cshtml, replace the contents of the file with the following code to replace the text about ASP.NET
and MVC with text about this app:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="jumbotron">
<h1>Contoso University</h1>
</div>
<div class="row">
<div class="col-md-4">
<h2>Welcome to Contoso University</h2>
<p>
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core Razor Pages web app.
</p>
</div>
<div class="col-md-4">
<h2>Build it from scratch</h2>
<p>You can build the application by following the steps in a series of tutorials.</p>
<p>
<a class="btn btn-default"
href="https://docs.microsoft.com/aspnet/core/data/ef-rp/intro">
See the tutorial »
</a>
</p>
</div>
<div class="col-md-4">
<h2>Download it</h2>
<p>You can download the completed project from GitHub.</p>
<p>
<a class="btn btn-default"
href="https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/data/ef-
rp/intro/samples/cu-final">
See project source code »
</a>
</p>
</div>
</div>
There's a one-to-many relationship between Student and Enrollment entities. There's a one-to-many relationship
between Course and Enrollment entities. A student can enroll in any number of courses. A course can have any
number of students enrolled in it.
In the following sections, a class for each one of these entities is created.
The Student entity
Create a Models folder. In the Models folder, create a class file named Student.cs with the following code:
using System;
using System.Collections.Generic;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
The ID property becomes the primary key column of the database (DB ) table that corresponds to this class. By
default, EF Core interprets a property that's named ID or classnameID as the primary key. In classnameID ,
classname is the name of the class. The alternative automatically recognized primary key is StudentID in the
preceding example.
The Enrollments property is a navigation property. Navigation properties link to other entities that are related to
this entity. In this case, the Enrollments property of a Student entity holds all of the Enrollment entities that are
related to that Student . For example, if a Student row in the DB has two related Enrollment rows, the Enrollments
navigation property contains those two Enrollment entities. A related Enrollment row is a row that contains that
student's primary key value in the StudentID column. For example, suppose the student with ID=1 has two rows in
the Enrollment table. The Enrollment table has two rows with StudentID = 1. StudentID is a foreign key in the
Enrollment table that specifies the student in the Student table.
If a navigation property can hold multiple entities, the navigation property must be a list type, such as
ICollection<T> . ICollection<T> can be specified, or a type such as List<T> or HashSet<T> . When ICollection<T>
is used, EF Core creates a HashSet<T> collection by default. Navigation properties that hold multiple entities come
from many-to-many and one-to-many relationships.
The Enrollment entity
The EnrollmentIDproperty is the primary key. This entity uses the classnameID pattern instead of ID like the
Student entity. Typically developers choose one pattern and use it throughout the data model. In a later tutorial,
using ID without classname is shown to make it easier to implement inheritance in the data model.
The Grade property is an enum . The question mark after the Grade type declaration indicates that the Grade
property is nullable. A grade that's null is different from a zero grade -- null means a grade isn't known or hasn't
been assigned yet.
The StudentID property is a foreign key, and the corresponding navigation property is Student . An Enrollment
entity is associated with one Student entity, so the property contains a single Student entity. The Student entity
differs from the Student.Enrollments navigation property, which contains multiple Enrollment entities.
The CourseID property is a foreign key, and the corresponding navigation property is Course . An Enrollment
entity is associated with one Course entity.
EF Core interprets a property as a foreign key if it's named <navigation property name><primary key property name> .
For example, StudentID for the Student navigation property, since the Student entity's primary key is ID .
Foreign key properties can also be named <primary key property name> . For example, CourseID since the Course
entity's primary key is CourseID .
The Course entity
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
The Enrollments property is a navigation property. A Course entity can be related to any number of Enrollment
entities.
The DatabaseGenerated attribute allows the app to specify the primary key rather than having the DB generate it.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));
}
The name of the connection string is passed in to the context by calling a method on a DbContextOptions object.
For local development, the ASP.NET Core configuration system reads the connection string from the
appsettings.json file.
Update main
In Program.cs, modify the Main method to do the following:
Get a DB context instance from the dependency injection container.
Call the EnsureCreated.
Dispose the context when the EnsureCreated method completes.
The following code shows the updated Program.cs file.
using ContosoUniversity.Models; // SchoolContext
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; // CreateScope
using Microsoft.Extensions.Logging;
using System;
namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context = services.GetRequiredService<SchoolContext>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
EnsureCreated ensures that the database for the context exists. If it exists, no action is taken. If it does not exist, then
the database and all its schema are created. EnsureCreated does not use migrations to create the database. A
database that is created with EnsureCreated cannot be later updated using migrations.
EnsureCreated is called on app start, which allows the following work flow:
Delete the DB.
Change the DB schema (for example, add an EmailAddress field).
Run the app.
EnsureCreated creates a DB with the EmailAddress column.
EnsureCreated is convenient early in development when the schema is rapidly evolving. Later in the tutorial the DB
is deleted and migrations are used.
Test the app
Run the app and accept the cookie policy. This app doesn't keep personal information. You can read about the
cookie policy at EU General Data Protection Regulation (GDPR ) support.
Select the Students link and then Create New.
Test the Edit, Details, and Delete links.
Examine the SchoolContext DB context
The main class that coordinates EF Core functionality for a given data model is the DB context class. The data
context is derived from Microsoft.EntityFrameworkCore.DbContext. The data context specifies which entities are
included in the data model. In this project, the class is named SchoolContext .
Update SchoolContext.cs with the following code:
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options)
: base(options)
{
}
The highlighted code creates a DbSet<TEntity> property for each entity set. In EF Core terminology:
An entity set typically corresponds to a DB table.
An entity corresponds to a row in the table.
DbSet<Enrollment> and DbSet<Course> could be omitted. EF Core includes them implicitly because the Student
entity references the Enrollment entity, and the Enrollment entity references the Course entity. For this tutorial,
keep DbSet<Enrollment> and DbSet<Course> in the SchoolContext .
SQL Server Express LocalDB
The connection string specifies SQL Server LocalDB. LocalDB is a lightweight version of the SQL Server Express
Database Engine and is intended for app development, not production use. LocalDB starts on demand and runs in
user mode, so there's no complex configuration. By default, LocalDB creates .mdf DB files in the C:/Users/<user>
directory.
using ContosoUniversity.Models;
using System;
using System.Linq;
namespace ContosoUniversity.Models
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// context.Database.EnsureCreated();
Note: The preceding code uses Models for the namespace ( namespace ContosoUniversity.Models ) rather than Data .
Models is consistent with the scaffolder -generated code. For more information, see this GitHub scaffolding issue.
The code checks if there are any students in the DB. If there are no students in the DB, the DB is initialized with test
data. It loads test data into arrays rather than List<T> collections to optimize performance.
The EnsureCreated method automatically creates the DB for the DB context. If the DB exists, EnsureCreated returns
without modifying the DB.
In Program.cs, modify the Main method to call Initialize :
try
{
var context = services.GetRequiredService<SchoolContext>();
// using ContosoUniversity.Data;
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
Delete any student records and restart the app. If the DB is not initialized, set a break point in Initialize to
diagnose the problem.
View the DB
The database name is generated from the context name you provided earlier plus a dash and a GUID. Thus, the
database name will be "SchoolContext-{GUID }". The GUID will be different for each user. Open SQL Server
Object Explorer (SSOX) from the View menu in Visual Studio. In SSOX, click (localdb)\MSSQLLocalDB >
Databases > SchoolContext-{GUID }.
Expand the Tables node.
Right-click the Student table and click View Data to see the columns created and the rows inserted into the table.
Asynchronous code
Asynchronous programming is the default mode for ASP.NET Core and EF Core.
A web server has a limited number of threads available, and in high load situations all of the available threads
might be in use. When that happens, the server can't process new requests until the threads are freed up. With
synchronous code, many threads may be tied up while they aren't actually doing any work because they're waiting
for I/O to complete. With asynchronous code, when a process is waiting for I/O to complete, its thread is freed up
for the server to use for processing other requests. As a result, asynchronous code enables server resources to be
used more efficiently, and the server is enabled to handle more traffic without delays.
Asynchronous code does introduce a small amount of overhead at run time. For low traffic situations, the
performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.
In the following code, the async keyword, Task<T> return value, await keyword, and ToListAsync method make
the code execute asynchronously.
Some things to be aware of when writing asynchronous code that uses EF Core:
Only statements that cause queries or commands to be sent to the DB are executed asynchronously. That
includes, ToListAsync , SingleOrDefaultAsync , FirstOrDefaultAsync , and SaveChangesAsync . It doesn't include
statements that just change an IQueryable , such as
var students = context.Students.Where(s => s.LastName == "Davolio") .
An EF Core context isn't thread safe: don't try to do multiple operations in parallel.
To take advantage of the performance benefits of async code, verify that library packages (such as for paging)
use async if they call EF Core methods that send queries to the DB.
For more information about asynchronous programming in .NET, see Async Overview and Asynchronous
programming with async and await.
In the next tutorial, basic CRUD (create, read, update, delete) operations are examined.
Additional resources
YouTube version of this tutorial
NEXT
Razor Pages with EF Core in ASP.NET Core - CRUD -
2 of 8
8/9/2019 • 22 minutes to read • Edit Online
No repository
Some developers use a service layer or repository pattern to create an abstraction layer between the UI (Razor
Pages) and the data access layer. This tutorial doesn't do that. To minimize complexity and keep the tutorial focused
on EF Core, EF Core code is added directly to the page model classes.
if (Student == null)
{
return NotFound();
}
return Page();
}
Replace the OnGetAsync method with the following code to read enrollment data for the selected student. The
changes are highlighted.
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
if (Student == null)
{
return NotFound();
}
return Page();
}
The Include and ThenInclude methods cause the context to load the Student.Enrollments navigation property, and
within each enrollment the Enrollment.Course navigation property. These methods are examined in detail in the
Reading related data tutorial.
The AsNoTracking method improves performance in scenarios where the entities returned are not updated in the
current context. AsNoTracking is discussed later in this tutorial.
Display enrollments
Replace the code in Pages/Students/Details.cshtml with the following code to display a list of enrollments. The
changes are highlighted.
@page
@model ContosoUniversity.Pages.Students.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>Student</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.Enrollments)
</dt>
<dd class="col-sm-10">
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Student.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
The preceding code loops through the entities in the Enrollments navigation property. For each enrollment, it
displays the course title and the grade. The course title is retrieved from the Course entity that's stored in the
Course navigation property of the Enrollments entity.
Run the app, select the Students tab, and click the Details link for a student. The list of courses and grades for the
selected student is displayed.
Ways to read one entity
The generated code uses FirstOrDefaultAsync to read one entity. This method returns null if nothing is found;
otherwise, it returns the first row found that satisfies the query filter criteria. FirstOrDefaultAsync is generally a
better choice than the following alternatives:
SingleOrDefaultAsync - Throws an exception if there's more than one entity that satisfies the query filter. To
determine if more than one row could be returned by the query, SingleOrDefaultAsync tries to fetch multiple
rows. This extra work is unnecessary if the query can only return one entity, as when it searches on a unique key.
FindAsync - Finds an entity with the primary key (PK). If an entity with the PK is being tracked by the context, it's
returned without a request to the database. This method is optimized to look up a single entity, but you can't call
Include with FindAsync . So if related data is needed, FirstOrDefaultAsync is the better choice.
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
_context.Students.Add(emptyStudent);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
TryUpdateModelAsync
The preceding code creates a Student object and then uses posted form fields to update the Student object's
properties. The TryUpdateModelAsync method:
Uses the posted form values from the PageContext property in the PageModel.
Updates only the properties listed ( s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate ).
Looks for form fields with a "student" prefix. For example, Student.FirstMidName . It's not case sensitive.
Uses the model binding system to convert form values from strings to the types in the Student model. For
example, EnrollmentDate has to be converted to DateTime.
Run the app, and create a student entity to test the Create page.
Overposting
Using TryUpdateModel to update fields with posted values is a security best practice because it prevents
overposting. For example, suppose the Student entity includes a Secret property that this web page shouldn't
update or add:
Even if the app doesn't have a Secret field on the create or update Razor Page, a hacker could set the Secret
value by overposting. A hacker could use a tool such as Fiddler, or write some JavaScript, to post a Secret form
value. The original code doesn't limit the fields that the model binder uses when it creates a Student instance.
Whatever value the hacker specified for the Secret form field is updated in the database. The following image
shows the Fiddler tool adding the Secret field (with the value "OverPost") to the posted form values.
The value "OverPost" is successfully added to the Secret property of the inserted row. That happens even though
the app designer never intended the Secret property to be set with the Create page.
View model
View models provide an alternative way to prevent overposting.
The application model is often called the domain model. The domain model typically contains all the properties
required by the corresponding entity in the database. The view model contains only the properties needed for the
UI that it is used for (for example, the Create page).
In addition to the view model, some apps use a binding model or input model to pass data between the Razor
Pages page model class and the browser.
Consider the following Student view model:
using System;
namespace ContosoUniversity.Models
{
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
}
The following code uses the StudentVM view model to create a new student:
[BindProperty]
public StudentVM StudentVM { get; set; }
The SetValues method sets the values of this object by reading values from another PropertyValues object.
SetValues uses property name matching. The view model type doesn't need to be related to the model type, it just
needs to have properties that match.
Using StudentVM requires Create.cshtml be updated to use StudentVM rather than Student .
if (Student == null)
{
return NotFound();
}
return Page();
}
if (studentToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
The code changes are similar to the Create page with a few exceptions:
FirstOrDefaultAsync has been replaced with FindAsync. When you don't have to include related data,
FindAsync is more efficient.
OnPostAsync has an id parameter.
The current student is fetched from the database, rather than creating an empty student.
Run the app, and test it by creating and editing a student.
Entity States
The database context keeps track of whether entities in memory are in sync with their corresponding rows in the
database. This tracking information determines what happens when SaveChangesAsync is called. For example,
when a new entity is passed to the AddAsync method, that entity's state is set to Added. When SaveChangesAsync is
called, the database context issues a SQL INSERT command.
An entity may be in one of the following states:
Added : The entity doesn't yet exist in the database. The SaveChanges method issues an INSERT statement.
Unchanged : No changes need to be saved with this entity. An entity has this status when it's read from the
database.
: Some or all of the entity's property values have been modified. The
Modified SaveChanges method issues
an UPDATE statement.
Deleted : The entity has been marked for deletion. The SaveChanges method issues a DELETE statement.
Detached : The entity isn't being tracked by the database context.
In a desktop app, state changes are typically set automatically. An entity is read, changes are made, and the entity
state is automatically changed to Modified . Calling SaveChanges generates a SQL UPDATE statement that updates
only the changed properties.
In a web app, the DbContext that reads an entity and displays the data is disposed after a page is rendered. When a
page's OnPostAsync method is called, a new web request is made and with a new instance of the DbContext .
Rereading the entity in that new context simulates desktop processing.
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }
if (Student == null)
{
return NotFound();
}
if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = "Delete failed. Try again";
}
return Page();
}
if (student == null)
{
return NotFound();
}
try
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction("./Delete",
new { id, saveChangesError = true });
}
}
}
}
The preceding code adds the optional parameter saveChangesError to the OnGetAsync method signature.
saveChangesError indicates whether the method was called after a failure to delete the student object. The delete
operation might fail because of transient network problems. Transient network errors are more likely when the
database is in the cloud. The saveChangesError parameter is false when the Delete page OnGetAsync is called from
the UI. When OnGetAsync is called by OnPostAsync (because the delete operation failed), the saveChangesError
parameter is true.
The method retrieves the selected entity, then calls the Remove method to set the entity's status to
OnPostAsync
Deleted . When SaveChanges is called, a SQL DELETE command is generated. If Remove fails:
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@Model.ErrorMessage</p>
<form method="post">
<input type="hidden" asp-for="Student.ID" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
Run the app and delete a student to test the Delete page.
Next steps
P R E V IO U S NEXT
T U T O R IA L T U T O R IA L
In this tutorial, the scaffolded CRUD (create, read, update, delete) code is reviewed and customized.
To minimize complexity and keep these tutorials focused on EF Core, EF Core code is used in the page models.
Some developers use a service layer or repository pattern in to create an abstraction layer between the UI (Razor
Pages) and the data access layer.
In this tutorial, the Create, Edit, Delete, and Details Razor Pages in the Students folder are examined.
The scaffolded code uses the following pattern for Create, Edit, and Delete pages:
Get and display the requested data with the HTTP GET method OnGetAsync .
Save changes to the data with the HTTP POST method OnPostAsync .
The Index and Details pages get and display the requested data with the HTTP GET method OnGetAsync
FindAsync
In much of the scaffolded code, FindAsync can be used in place of FirstOrDefaultAsync .
FindAsync :
Finds an entity with the primary key (PK). If an entity with the PK is being tracked by the context, it's returned
without a request to the DB.
Is simple and concise.
Is optimized to look up a single entity.
Can have perf benefits in some situations, but that rarely happens for typical web apps.
Implicitly uses FirstAsync instead of SingleAsync.
But if you want to Include other entities, then FindAsync is no longer appropriate. This means that you may need
to abandon FindAsync and move to a query as your app progresses.
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
Run the app and select a Details link. The URL is of the form http://localhost:5000/Students/Details?id=2 . The
Student ID is passed using a query string ( ?id=2 ).
Update the Edit, Details, and Delete Razor Pages to use the "{id:int}" route template. Change the page directive
for each of these pages from @page to @page "{id:int}" .
A request to the page with the "{id:int}" route template that does not include a integer route value returns an HTTP
404 (not found) error. For example, http://localhost:5000/Students/Details returns a 404 error. To make the ID
optional, append ? to the route constraint:
@page "{id:int?}"
Run the app, click on a Details link, and verify the URL is passing the ID as route data (
http://localhost:5000/Students/Details/2 ).
Don't globally change @page to @page "{id:int}" , doing so breaks the links to the Home and Create pages.
Add related data
The scaffolded code for the Students Index page doesn't include the Enrollments property. In this section, the
contents of the Enrollments collection is displayed in the Details page.
The OnGetAsync method of Pages/Students/Details.cshtml.cs uses the FirstOrDefaultAsync method to retrieve a
single Student entity. Add the following highlighted code:
if (Student == null)
{
return NotFound();
}
return Page();
}
The Include and ThenInclude methods cause the context to load the Student.Enrollments navigation property, and
within each enrollment the Enrollment.Course navigation property. These methods are examined in detail in the
reading-related data tutorial.
The AsNoTracking method improves performance in scenarios when the entities returned are not updated in the
current context. AsNoTracking is discussed later in this tutorial.
Display related enrollments on the Details page
Open Pages/Students/Details.cshtml. Add the following highlighted code to display a list of enrollments:
@page "{id:int}"
@model ContosoUniversity.Pages.Students.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Student</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.Enrollments)
</dt>
<dd>
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Student.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
If code indentation is wrong after the code is pasted, press CTRL -K-D to correct it.
The preceding code loops through the entities in the Enrollments navigation property. For each enrollment, it
displays the course title and the grade. The course title is retrieved from the Course entity that's stored in the
Course navigation property of the Enrollments entity.
Run the app, select the Students tab, and click the Details link for a student. The list of courses and grades for the
selected student is displayed.
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
_context.Student.Add(emptyStudent);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return null;
}
TryUpdateModelAsync
Examine the TryUpdateModelAsync code:
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
In the preceding code, TryUpdateModelAsync<Student> tries to update the emptyStudent object using the posted form
values from the PageContext property in the PageModel. TryUpdateModelAsync only updates the properties listed (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate ).
Even if the app doesn't have a Secret field on the create/update Razor Page, a hacker could set the Secret value
by overposting. A hacker could use a tool such as Fiddler, or write some JavaScript, to post a Secret form value.
The original code doesn't limit the fields that the model binder uses when it creates a Student instance.
Whatever value the hacker specified for the Secret form field is updated in the DB. The following image shows the
Fiddler tool adding the Secret field (with the value "OverPost") to the posted form values.
The value "OverPost" is successfully added to the Secret property of the inserted row. The app designer never
intended the Secret property to be set with the Create page.
View model
A view model typically contains a subset of the properties included in the model used by the application. The
application model is often called the domain model. The domain model typically contains all the properties
required by the corresponding entity in the DB. The view model contains only the properties needed for the UI
layer (for example, the Create page). In addition to the view model, some apps use a binding model or input model
to pass data between the Razor Pages page model class and the browser. Consider the following Student view
model:
using System;
namespace ContosoUniversity.Models
{
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
}
View models provide an alternative way to prevent overposting. The view model contains only the properties to
view (display) or update.
The following code uses the StudentVM view model to create a new student:
[BindProperty]
public StudentVM StudentVM { get; set; }
The SetValues method sets the values of this object by reading values from another PropertyValues object.
SetValues uses property name matching. The view model type doesn't need to be related to the model type, it just
needs to have properties that match.
Using StudentVM requires CreateVM.cshtml be updated to use StudentVM rather than Student .
In Razor Pages, the PageModel derived class is the view model.
[BindProperty]
public Student Student { get; set; }
if (Student == null)
{
return NotFound();
}
return Page();
}
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
}
The code changes are similar to the Create page with a few exceptions:
OnPostAsync has an optional id parameter.
The current student is fetched from the DB, rather than creating an empty student.
FirstOrDefaultAsync has been replaced with FindAsync. FindAsync is a good choice when selecting an entity
from the primary key. See FindAsync for more information.
Test the Edit and Create pages
Create and edit a few student entities.
Entity States
The DB context keeps track of whether entities in memory are in sync with their corresponding rows in the DB. The
DB context sync information determines what happens when SaveChangesAsync is called. For example, when a
new entity is passed to the AddAsync method, that entity's state is set to Added. When SaveChangesAsync is called,
the DB context issues a SQL INSERT command.
An entity may be in one of the following states:
Added : The entity doesn't yet exist in the DB. The SaveChanges method issues an INSERT statement.
Unchanged : No changes need to be saved with this entity. An entity has this status when it's read from the
DB.
: Some or all of the entity's property values have been modified. The
Modified SaveChanges method issues
an UPDATE statement.
Deleted : The entity has been marked for deletion. The SaveChanges method issues a DELETE statement.
Detached : The entity isn't being tracked by the DB context.
In a desktop app, state changes are typically set automatically. An entity is read, changes are made, and the entity
state to automatically be changed to Modified . Calling SaveChanges generates a SQL UPDATE statement that
updates only the changed properties.
In a web app, the DbContext that reads an entity and displays the data is disposed after a page is rendered. When a
page's OnPostAsync method is called, a new web request is made and with a new instance of the DbContext . Re-
reading the entity in that new context simulates desktop processing.
[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }
if (Student == null)
{
return NotFound();
}
if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = "Delete failed. Try again";
}
return Page();
}
The preceding code contains the optional parameter saveChangesError . saveChangesError indicates whether the
method was called after a failure to delete the student object. The delete operation might fail because of transient
network problems. Transient network errors are more likely in the cloud. saveChangesError is false when the Delete
page OnGetAsync is called from the UI. When OnGetAsync is called by OnPostAsync (because the delete operation
failed), the saveChangesError parameter is true.
The Delete pages OnPostAsync method
Replace the OnPostAsync with the following code:
if (student == null)
{
return NotFound();
}
try
{
_context.Student.Remove(student);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction("./Delete",
new { id, saveChangesError = true });
}
}
The preceding code retrieves the selected entity, then calls the Remove method to set the entity's status to Deleted .
When SaveChanges is called, a SQL DELETE command is generated. If Remove fails:
The DB exception is caught.
The Delete pages OnGetAsync method is called with saveChangesError=true .
Update the Delete Razor Page
Add the following highlighted error message to the Delete Razor Page.
@page "{id:int}"
@model ContosoUniversity.Pages.Students.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ErrorMessage</p>
Test Delete.
Common errors
Students/Index or other links don't work:
Verify the Razor Page contains the correct @page directive. For example, The Students/Index Razor Page should
not contain a route template:
@page "{id:int}"
Additional resources
YouTube version of this tutorial
P R E V IO U S NEXT
Razor Pages with EF Core in ASP.NET Core - Sort,
Filter, Paging - 3 of 8
8/9/2019 • 29 minutes to read • Edit Online
Add sorting
Replace the code in Pages/Students/Index.cshtml.cs with the following code to add sorting.
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
The OnGetAsync method receives a sortOrder parameter from the query string in the URL. The URL (including the
query string) is generated by the Anchor Tag Helper.
The sortOrder parameter is either "Name" or "Date." The sortOrder parameter is optionally followed by "_desc"
to specify descending order. The default sort order is ascending.
When the Index page is requested from the Students link, there's no query string. The students are displayed in
ascending order by last name. Ascending order by last name is the default (fall-through case) in the switch
statement. When the user clicks a column heading link, the appropriate sortOrder value is provided in the query
string value.
NameSort and DateSort are used by the Razor Page to configure the column heading hyperlinks with the
appropriate query string values:
The code uses the C# conditional operator ?:. The ?: operator is a ternary operator (it takes three operands). The
first line specifies that when sortOrder is null or empty, NameSort is set to "name_desc." If sortOrder is not null or
empty, NameSort is set to an empty string.
These two statements enable the page to set the column heading hyperlinks as follows:
The method uses LINQ to Entities to specify the column to sort by. The code initializes an IQueryable<Student>
before the switch statement, and modifies it in the switch statement:
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
When an IQueryable is created or modified, no query is sent to the database. The query isn't executed until the
IQueryable object is converted into a collection. IQueryable are converted to a collection by calling a method such
as ToListAsync . Therefore, the IQueryable code results in a single query that's not executed until the following
statement:
OnGetAsync could get verbose with a large number of sortable columns. For information about an alternative way
to code this functionality, see Use dynamic LINQ to simplify code in the MVC version of this tutorial series.
Add column heading hyperlinks to the Student Index page
Replace the code in Students/Index.cshtml, with the following code. The changes are highlighted.
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
The preceding code:
Adds hyperlinks to the LastName and EnrollmentDate column headings.
Uses the information in NameSort and DateSort to set up hyperlinks with the current sort order values.
Changes the page heading from Index to Students.
Changes Model.Student to Model.Students .
Add filtering
To add filtering to the Students Index page:
A text box and a submit button is added to the Razor Page. The text box supplies a search string on the first or
last name.
The page model is updated to use the text box value.
Update the OnGetAsync method
Replace the code in Students/Index.cshtml.cs with the following code to add filtering:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
CurrentFilter = searchString;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
For example, the .NET Framework implementation of Contains performs a case-sensitive comparison by default.
In SQL Server, Contains case-sensitivity is determined by the collation setting of the SQL Server instance. SQL
Server defaults to case-insensitive. SQLite defaults to case-sensitive. ToUpper could be called to make the test
explicitly case-insensitive:
The preceding code would ensure that the filter is case-insensitive even if the Where method is called on an
IEnumerable or runs on SQLite.
When Contains is called on an IEnumerable collection, the .NET Core implementation is used. When Contains is
called on an IQueryable object, the database implementation is used.
Calling Contains on an IQueryable is usually preferable for performance reasons. With IQueryable , the filtering is
done by the database server. If an IEnumerable is created first, all the rows have to be returned from the database
server.
There's a performance penalty for calling ToUpper . The ToUpper code adds a function in the WHERE clause of the
TSQL SELECT statement. The added function prevents the optimizer from using an index. Given that SQL is
installed as case-insensitive, it's best to avoid the ToUpper call when it's not needed.
For more information, see How to use case-insensitive query with Sqlite provider.
Update the Razor page
Replace the code in Pages/Students/Index.cshtml to create a Search button and assorted chrome.
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
The preceding code uses the <form> tag helper to add the search text box and button. By default, the <form> tag
helper submits form data with a POST. With POST, the parameters are passed in the HTTP message body and not
in the URL. When HTTP GET is used, the form data is passed in the URL as query strings. Passing the data with
query strings enables users to bookmark the URL. The W3C guidelines recommend that GET should be used when
the action doesn't result in an update.
Test the app:
Select the Students tab and enter a search string. If you're using SQLite, the filter is case-insensitive only if
you implemented the optional ToUpper code shown earlier.
Select Search.
Notice that the URL contains the search string. For example:
https://localhost:<port>/Students?SearchString=an
If the page is bookmarked, the bookmark contains the URL to the page and the SearchString query string. The
method="get" in the form tag is what caused the query string to be generated.
Currently, when a column heading sort link is selected, the filter value from the Search box is lost. The lost filter
value is fixed in the next section.
Add paging
In this section, a PaginatedList class is created to support paging. The PaginatedList class uses Skip and Take
statements to filter data on the server instead of retrieving all rows of the table. The following illustration shows the
paging buttons.
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
this.AddRange(items);
}
The CreateAsyncmethod in the preceding code takes page size and page number and applies the appropriate
Skip and Take statements to the IQueryable . When ToListAsync is called on the IQueryable , it returns a List
containing only the requested page. The properties HasPreviousPage and HasNextPage are used to enable or
disable Previous and Next paging buttons.
The CreateAsync method is used to create the PaginatedList<T> . A constructor can't create the PaginatedList<T>
object; constructors can't run asynchronous code.
Add paging to the PageModel class
Replace the code in Students/Index.cshtml.cs to add paging.
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
CurrentFilter = searchString;
int pageSize = 3;
Students = await PaginatedList<Student>.CreateAsync(
studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
}
}
}
The PaginatedList.CreateAsync method converts the student query to a single page of students in a collection type
that supports paging. That single page of students is passed to the Razor Page.
The two question marks after pageIndex in the PaginatedList.CreateAsync call represent the null-coalescing
operator. The null-coalescing operator defines a default value for a nullable type. The expression (pageIndex ?? 1)
means return the value of pageIndex if it has a value. If pageIndex doesn't have a value, return 1.
Add paging links to the Razor Page
Replace the code in Students/Index.cshtml with the following code. The changes are highlighted:
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.Students.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Students.HasNextPage ? "disabled" : "";
}
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @nextDisabled">
Next
</a>
The column header links use the query string to pass the current search string to the OnGetAsync method:
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @nextDisabled">
Next
</a>
Add grouping
This section creates an About page that displays how many students have enrolled for each enrollment date. The
update uses grouping and includes the following steps:
Create a view model for the data used by the About page.
Update the About page to use the view model.
Create the view model
Create a Models/SchoolViewModels folder.
Create SchoolViewModels/EnrollmentDateGroup.cs with the following code:
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
@page
@model ContosoUniversity.Pages.AboutModel
@{
ViewData["Title"] = "Student Body Statistics";
}
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;
The LINQ statement groups the student entities by enrollment date, calculates the number of entities in each
group, and stores the results in a collection of EnrollmentDateGroup view model objects.
Run the app and navigate to the About page. The count of students for each enrollment date is displayed in a table.
Next steps
In the next tutorial, the app uses migrations to update the data model.
P R E V IO U S NEXT
T U T O R IA L T U T O R IA L
If you run into problems you can't solve, download the completed app.
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
The preceding code receives a sortOrder parameter from the query string in the URL. The URL (including the
query string) is generated by the Anchor Tag Helper
The sortOrder parameter is either "Name" or "Date." The sortOrder parameter is optionally followed by "_desc"
to specify descending order. The default sort order is ascending.
When the Index page is requested from the Students link, there's no query string. The students are displayed in
ascending order by last name. Ascending order by last name is the default (fall-through case) in the switch
statement. When the user clicks a column heading link, the appropriate sortOrder value is provided in the query
string value.
NameSort and DateSort are used by the Razor Page to configure the column heading hyperlinks with the
appropriate query string values:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
The first line specifies that when sortOrder is null or empty, NameSort is set to "name_desc." If sortOrder is not
null or empty, NameSort is set to an empty string.
The ?: operator is also known as the ternary operator.
These two statements enable the page to set the column heading hyperlinks as follows:
The method uses LINQ to Entities to specify the column to sort by. The code initializes an IQueryable<Student>
before the switch statement, and modifies it in the switch statement:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
When an IQueryable is created or modified, no query is sent to the database. The query isn't executed until the
IQueryable object is converted into a collection. IQueryable are converted to a collection by calling a method such
as ToListAsync . Therefore, the IQueryable code results in a single query that's not executed until the following
statement:
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
The preceding code would ensure that results are case-insensitive if the code changes to use IEnumerable . When
Contains is called on an IEnumerable collection, the .NET Core implementation is used. When Contains is called
on an IQueryable object, the database implementation is used. Returning an IEnumerable from a repository can
have a significant performance penalty:
1. All the rows are returned from the DB server.
2. The filter is applied to all the returned rows in the application.
There's a performance penalty for calling ToUpper . The ToUpper code adds a function in the WHERE clause of the
TSQL SELECT statement. The added function prevents the optimizer from using an index. Given that SQL is
installed as case-insensitive, it's best to avoid the ToUpper call when it's not needed.
Add a Search Box to the Student Index page
In Pages/Students/Index.cshtml, add the following highlighted code to create a Search button and assorted
chrome.
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
The preceding code uses the <form> tag helper to add the search text box and button. By default, the <form> tag
helper submits form data with a POST. With POST, the parameters are passed in the HTTP message body and not
in the URL. When HTTP GET is used, the form data is passed in the URL as query strings. Passing the data with
query strings enables users to bookmark the URL. The W3C guidelines recommend that GET should be used when
the action doesn't result in an update.
Test the app:
Select the Students tab and enter a search string.
Select Search.
Notice that the URL contains the search string.
http://localhost:5000/Students?SearchString=an
If the page is bookmarked, the bookmark contains the URL to the page and the SearchString query string. The
method="get" in the form tag is what caused the query string to be generated.
Currently, when a column heading sort link is selected, the filter value from the Search box is lost. The lost filter
value is fixed in the next section.
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
this.AddRange(items);
}
The CreateAsyncmethod in the preceding code takes page size and page number and applies the appropriate
Skip and Take statements to the IQueryable . When ToListAsync is called on the IQueryable , it returns a List
containing only the requested page. The properties HasPreviousPage and HasNextPage are used to enable or
disable Previous and Next paging buttons.
The CreateAsync method is used to create the PaginatedList<T> . A constructor can't create the PaginatedList<T>
object, constructors can't run asynchronous code.
CurrentFilter = searchString;
int pageSize = 3;
Student = await PaginatedList<Student>.CreateAsync(
studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
The preceding code adds the page index, the current sortOrder , and the currentFilter to the method signature.
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
The PaginatedList.CreateAsync method converts the student query to a single page of students in a collection type
that supports paging. That single page of students is passed to the Razor Page.
The two question marks in PaginatedList.CreateAsync represent the null-coalescing operator. The null-coalescing
operator defines a default value for a nullable type. The expression (pageIndex ?? 1) means return the value of
pageIndex if it has a value. If pageIndex doesn't have a value, return 1.
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>
The column header links use the query string to pass the current search string to the OnGetAsync method so that
the user can sort within filter results:
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;
The LINQ statement groups the student entities by enrollment date, calculates the number of entities in each
group, and stores the results in a collection of EnrollmentDateGroup view model objects.
Modify the About Razor Page
Replace the code in the Pages/About.cshtml file with the following code:
@page
@model ContosoUniversity.Pages.AboutModel
@{
ViewData["Title"] = "Student Body Statistics";
}
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
Run the app and navigate to the About page. The count of students for each enrollment date is displayed in a table.
If you run into problems you can't solve, download the completed app for this stage.
Additional resources
Debugging ASP.NET Core 2.x source
YouTube version of this tutorial
In the next tutorial, the app uses migrations to update the data model.
P R E V IO U S NEXT
Razor Pages with EF Core in ASP.NET Core -
Migrations - 4 of 8
8/9/2019 • 11 minutes to read • Edit Online
NOTE
SQLite limitations
This tutorial uses the Entity Framework Core migrations feature where possible. Migrations updates the database schema to
match changes in the data model. However, migrations only does the kinds of changes that the database engine supports,
and SQLite's schema change capabilities are limited. For example, adding a column is supported, but removing a column is
not supported. If a migration is created to remove a column, the ef migrations add command succeeds but the
ef database update command fails.
The workaround for the SQLite limitations is to manually write migrations code to perform a table rebuild when something in
the table changes. The code would go in the Up and Down methods for a migration and would involve:
Creating a new table.
Copying data from the old table to the new table.
Dropping the old table.
Renaming the new table.
Writing database-specific code of this type is outside the scope of this tutorial. Instead, this tutorial drops and re-creates the
database whenever an attempt to apply a migration would fail. For more information, see the following resources:
SQLite EF Core Database Provider Limitations
Customize migration code
Data seeding
SQLite ALTER TABLE statement
Drop the database
Visual Studio
Visual Studio Code
Use SQL Server Object Explorer (SSOX) to delete the database, or run the following command in the Package
Manager Console (PMC ):
Drop-Database
Add-Migration InitialCreate
Update-Database
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ContosoUniversity.Migrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Course",
columns: table => new
{
CourseID = table.Column<int>(nullable: false),
Title = table.Column<string>(nullable: true),
Credits = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Course", x => x.CourseID);
});
migrationBuilder.CreateTable(
name: "Student",
columns: table => new
{
ID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
LastName = table.Column<string>(nullable: true),
FirstMidName = table.Column<string>(nullable: true),
EnrollmentDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Student", x => x.ID);
});
migrationBuilder.CreateTable(
name: "Enrollment",
columns: table => new
{
EnrollmentID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
CourseID = table.Column<int>(nullable: false),
StudentID = table.Column<int>(nullable: false),
Grade = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Enrollment", x => x.EnrollmentID);
table.ForeignKey(
name: "FK_Enrollment_Course_CourseID",
column: x => x.CourseID,
principalTable: "Course",
principalColumn: "CourseID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Enrollment_Student_StudentID",
column: x => x.StudentID,
principalTable: "Student",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Enrollment_CourseID",
table: "Enrollment",
column: "CourseID");
migrationBuilder.CreateIndex(
name: "IX_Enrollment_StudentID",
table: "Enrollment",
column: "StudentID");
}
migrationBuilder.DropTable(
name: "Course");
migrationBuilder.DropTable(
name: "Student");
}
}
}
Remove EnsureCreated
This tutorial series started by using EnsureCreated . EnsureCreated doesn't create a migrations history table and so
can't be used with migrations. It's designed for testing or rapid prototyping where the database is dropped and re-
created frequently.
From this point forward, the tutorials will use migrations.
In Data/DBInitializer.cs, comment out the following line:
context.Database.EnsureCreated();
Troubleshooting
If the app uses SQL Server LocalDB and displays the following exception:
Next steps
The next tutorial builds out the data model, adding entity properties and new entities.
P R E V IO U S NEXT
T U T O R IA L T U T O R IA L
In this tutorial, the EF Core migrations feature for managing data model changes is used.
If you run into problems you can't solve, download the completed app.
When a new app is developed, the data model changes frequently. Each time the model changes, the model gets
out of sync with the database. This tutorial started by configuring the Entity Framework to create the database if it
doesn't exist. Each time the data model changes:
The DB is dropped.
EF creates a new one that matches the model.
The app seeds the DB with test data.
This approach to keeping the DB in sync with the data model works well until you deploy the app to production.
When the app is running in production, it's usually storing data that needs to be maintained. The app can't start
with a test DB each time a change is made (such as adding a new column). The EF Core Migrations feature solves
this problem by enabling EF Core to update the DB schema instead of creating a new DB.
Rather than dropping and recreating the DB when the data model changes, migrations updates the schema and
retains existing data.
Drop-Database
migrationBuilder.CreateTable(
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Enrollment");
migrationBuilder.DropTable(
name: "Course");
migrationBuilder.DropTable(
name: "Student");
}
}
Migrations calls the Up method to implement the data model changes for a migration. When you enter a
command to roll back the update, migrations calls the Down method.
The preceding code is for the initial migration. That code was created when the migrations add InitialCreate
command was run. The migration name parameter ("InitialCreate" in the example) is used for the file name. The
migration name can be any valid file name. It's best to choose a word or phrase that summarizes what is being
done in the migration. For example, a migration that added a department table might be called
"AddDepartmentTable."
If the initial migration is created and the DB exists:
The DB creation code is generated.
The DB creation code doesn't need to run because the DB already matches the data model. If the DB creation
code is run, it doesn't make any changes because the DB already matches the data model.
When the app is deployed to a new environment, the DB creation code must be run to create the DB.
Previously the DB was dropped and doesn't exist, so migrations creates the new DB.
The data model snapshot
Migrations create a snapshot of the current database schema in Migrations/SchoolContextModelSnapshot.cs. When
you add a migration, EF determines what changed by comparing the data model to the snapshot file.
To delete a migration, use the following command:
Visual Studio
Visual Studio Code
Remove-Migration
The remove migrations command deletes the migration and ensures the snapshot is correctly reset.
Remove EnsureCreated and test the app
For early development, EnsureCreated was used. In this tutorial, migrations are used. EnsureCreated has the
following limitations:
Bypasses migrations and creates the DB and schema.
Doesn't create a migrations table.
Can not be used with migrations.
Is designed for testing or rapid prototyping where the DB is dropped and re-created frequently.
Remove EnsureCreated :
context.Database.EnsureCreated();
EF Core uses the __MigrationsHistory table to see if any migrations need to run. If the DB is up-to-date, no
migration is run.
Troubleshooting
Download the completed app.
The app generates the following exception:
SqlException: Cannot open database "ContosoUniversity" requested by the login.
The login failed.
Login failed for user 'user name'.
Additional resources
YouTube version of this tutorial
.NET Core CLI.
Package Manager Console (Visual Studio)
P R E V IO U S NEXT
Razor Pages with EF Core in ASP.NET Core - Data
Model - 5 of 8
8/9/2019 • 56 minutes to read • Edit Online
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
The preceding code adds a FullName property and adds the following attributes to existing properties:
[DataType]
[DisplayFormat]
[StringLength]
[Column]
[Required]
[Display]
[DataType(DataType.Date)]
For student enrollment dates, all of the pages currently display the time of day along with the date, although only
the date is relevant. By using data annotation attributes, you can make one code change that will fix the display
format in every page that shows the data.
The DataType attribute specifies a data type that's more specific than the database intrinsic type. In this case only
the date should be displayed, not the date and time. The DataType Enumeration provides for many data types, such
as Date, Time, PhoneNumber, Currency, EmailAddress, etc. The DataType attribute can also enable the app to
automatically provide type-specific features. For example:
The mailto: link is automatically created for DataType.EmailAddress .
The date selector is provided for DataType.Date in most browsers.
The DataType attribute emits HTML 5 data- (pronounced data dash) attributes. The DataType attributes don't
provide validation.
The DisplayFormat attribute
DataType.Date doesn't specify the format of the date that's displayed. By default, the date field is displayed
according to the default formats based on the server's CultureInfo.
The DisplayFormat attribute is used to explicitly specify the date format. The ApplyFormatInEditMode setting
specifies that the formatting should also be applied to the edit UI. Some fields shouldn't use ApplyFormatInEditMode .
For example, the currency symbol should generally not be displayed in an edit text box.
The DisplayFormat attribute can be used by itself. It's generally a good idea to use the DataType attribute with the
DisplayFormat attribute. The DataType attribute conveys the semantics of the data as opposed to how to render it
on a screen. The DataType attribute provides the following benefits that are not available in DisplayFormat :
The browser can enable HTML5 features. For example, show a calendar control, the locale-appropriate currency
symbol, email links, and client-side input validation.
By default, the browser renders data using the correct format based on the locale.
For more information, see the <input> Tag Helper documentation.
The StringLength attribute
Data validation rules and validation error messages can be specified with attributes. The StringLength attribute
specifies the minimum and maximum length of characters that are allowed in a data field. The code shown limits
names to no more than 50 characters. An example that sets the minimum string length is shown later.
The StringLength attribute also provides client-side and server-side validation. The minimum value has no impact
on the database schema.
The StringLength attribute doesn't prevent a user from entering white space for a name. The RegularExpression
attribute can be used to apply restrictions to the input. For example, the following code requires the first character
to be upper case and the remaining characters to be alphabetical:
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
Visual Studio
Visual Studio Code
In SQL Server Object Explorer (SSOX), open the Student table designer by double-clicking the Student table.
The preceding image shows the schema for the Student table. The name fields have type nvarchar(MAX) . When a
migration is created and applied later in this tutorial, the name fields become nvarchar(50) as a result of the string
length attributes.
The Column attribute
[Column("FirstName")]
public string FirstMidName { get; set; }
Attributes can control how classes and properties are mapped to the database. In the Student model, the Column
attribute is used to map the name of the FirstMidName property to "FirstName" in the database.
When the database is created, property names on the model are used for column names (except when the Column
attribute is used). The Student model uses FirstMidName for the first-name field because the field might also
contain a middle name.
With the [Column] attribute, Student.FirstMidName in the data model maps to the FirstName column of the
Student table. The addition of the Column attribute changes the model backing the SchoolContext . The model
backing the SchoolContext no longer matches the database. That discrepancy will be resolved by adding a
migration later in this tutorial.
The Required attribute
[Required]
The Required attribute makes the name properties required fields. The Required attribute isn't needed for non-
nullable types such as value types (for example, DateTime , int , and double ). Types that can't be null are
automatically treated as required fields.
The Required attribute could be replaced with a minimum length parameter in the StringLength attribute:
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
The Display attribute specifies that the caption for the text boxes should be "First Name", "Last Name", "Full
Name", and "Enrollment Date." The default captions had no space dividing the words, for example "Lastname."
Create a migration
Run the app and go to the Students page. An exception is thrown. The [Column] attribute causes EF to expect to
find a column named FirstName , but the column name in the database is still FirstMidName .
Visual Studio
Visual Studio Code
The error message is similar to the following example:
In the PMC, enter the following commands to create a new migration and update the database:
Add-Migration ColumnFirstName
Update-Database
The warning is generated because the name fields are now limited to 50 characters. If a name in the
database had more than 50 characters, the 51 to last character would be lost.
Open the Student table in SSOX:
Before the migration was applied, the name columns were of type nvarchar(MAX). The name columns are
now nvarchar(50) . The column name has changed from FirstMidName to FirstName .
NOTE
In the following sections, building the app at some stages generates compiler errors. The instructions specify when to build
the app.
namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
Multiple attributes can be on one line. The HireDate attributes could be written as follows:
Navigation properties
The CourseAssignments and OfficeAssignment properties are navigation properties.
An instructor can teach any number of courses, so CourseAssignments is defined as a collection.
An instructor can have at most one office, so the OfficeAssignment property holds a single OfficeAssignment entity.
OfficeAssignment is null if no office is assigned.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
[Key]
public int InstructorID { get; set; }
By default, EF Core treats the key as non-database-generated because the column is for an identifying relationship.
The Instructor navigation property
The Instructor.OfficeAssignment navigation property can be null because there might not be an OfficeAssignment
row for a given instructor. An instructor might not have an office assignment.
The OfficeAssignment.Instructor navigation property will always have an instructor entity because the foreign key
InstructorID -
type is int , a non nullable value type. An office assignment can't exist without an instructor.
When an Instructor entity has a related OfficeAssignment entity, each entity has a reference to the other one in its
navigation property.
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
The Course entity has a foreign key (FK) property DepartmentID . DepartmentID points to the related Department
entity. The Course entity has a Department navigation property.
EF Core doesn't require a foreign key property for a data model when the model has a navigation property for a
related entity. EF Core automatically creates FKs in the database wherever they're needed. EF Core creates shadow
properties for automatically created FKs. However, explicitly including the FK in the data model can make updates
simpler and more efficient. For example, consider a model where the FK property DepartmentID is not included.
When a course entity is fetched to edit:
The Department property is null if it's not explicitly loaded.
To update the course entity, the Department entity must first be fetched.
When the FK property DepartmentID is included in the data model, there's no need to fetch the Department entity
before an update.
The DatabaseGenerated attribute
The [DatabaseGenerated(DatabaseGeneratedOption.None)] attribute specifies that the PK is provided by the
application rather than generated by the database.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
By default, EF Core assumes that PK values are generated by the database. Database-generated is generally the
best approach. For Course entities, the user specifies the PK. For example, a course number such as a 1000 series
for the math department, a 2000 series for the English department.
The DatabaseGenerated attribute can also be used to generate default values. For example, the database can
automatically generate a date field to record the date a row was created or updated. For more information, see
Generated Properties.
Foreign key and navigation properties
The foreign key (FK) properties and navigation properties in the Course entity reflect the following relationships:
A course is assigned to one department, so there's a DepartmentID FK and a Department navigation property.
A course can have any number of students enrolled in it, so the Enrollments navigation property is a collection:
A course may be taught by multiple instructors, so the CourseAssignments navigation property is a collection:
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
[Column(TypeName="money")]
public decimal Budget { get; set; }
Column mapping is generally not required. EF Core chooses the appropriate SQL Server data type based on the
CLR type for the property. The CLR decimal type maps to a SQL Server decimal type. Budget is for currency, and
the money data type is more appropriate for currency.
Foreign key and navigation properties
The FK and navigation properties reflect the following relationships:
A department may or may not have an administrator.
An administrator is always an instructor. Therefore the InstructorID property is included as the FK to the
Instructor entity.
The question mark (?) in the preceding code specifies the property is nullable.
A department may have many courses, so there's a Courses navigation property:
public ICollection<Course> Courses { get; set; }
By convention, EF Core enables cascade delete for non-nullable FKs and for many-to-many relationships. This
default behavior can result in circular cascade delete rules. Circular cascade delete rules cause an exception when a
migration is added.
For example, if the Department.InstructorID property was defined as non-nullable, EF Core would configure a
cascade delete rule. In that case, the department would be deleted when the instructor assigned as its administrator
is deleted. In this scenario, a restrict rule would make more sense. The following fluent API would set a restrict rule
and disable cascade delete.
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
An enrollment record is for one student, so there's a StudentID FK property and a Student navigation property:
Many-to-Many Relationships
There's a many-to-many relationship between the Student and Course entities. The Enrollment entity functions
as a many-to-many join table with payload in the database. "With payload" means that the Enrollment table
contains additional data besides FKs for the joined tables (in this case, the PK and Grade ).
The following illustration shows what these relationships look like in an entity diagram. (This diagram was
generated using EF Power Tools for EF 6.x. Creating the diagram isn't part of the tutorial.)
Each relationship line has a 1 at one end and an asterisk (*) at the other, indicating a one-to-many relationship.
If the table didn't include grade information, it would only need to contain the two FKs ( CourseID and
Enrollment
StudentID . A many-to-many join table without payload is sometimes called a pure join table ( PJT ).
)
The Instructor and Course entities have a many-to-many relationship using a pure join table.
Note: EF 6.x supports implicit join tables for many-to-many relationships, but EF Core doesn't. For more
information, see Many-to-many relationships in EF Core 2.0.
namespace ContosoUniversity.Models
{
public class CourseAssignment
{
public int InstructorID { get; set; }
public int CourseID { get; set; }
public Instructor Instructor { get; set; }
public Course Course { get; set; }
}
}
The Instructor-to-Courses many-to-many relationship requires a join table, and the entity for that join table is
CourseAssignment.
It's common to name a join entity EntityName1EntityName2 . For example, the Instructor-to-Courses join table using
this pattern would be CourseInstructor . However, we recommend using a name that describes the relationship.
Data models start out simple and grow. Join tables without payload (PJTs) frequently evolve to include payload. By
starting with a descriptive entity name, the name doesn't need to change when the join table changes. Ideally, the
join entity would have its own natural (possibly single word) name in the business domain. For example, Books and
Customers could be linked with a join entity called Ratings. For the Instructor-to-Courses many-to-many
relationship, CourseAssignment is preferred over CourseInstructor .
Composite key
The two FKs in CourseAssignment ( InstructorID and CourseID ) together uniquely identify each row of the
CourseAssignment table. CourseAssignment doesn't require a dedicated PK. The InstructorID and CourseID
properties function as a composite PK. The only way to specify composite PKs to EF Core is with the fluent API.
The next section shows how to configure the composite PK.
The composite key ensures that:
Multiple rows are allowed for one course.
Multiple rows are allowed for one instructor.
Multiple rows aren't allowed for the same instructor and course.
The Enrollment join entity defines its own PK, so duplicates of this sort are possible. To prevent such duplicates:
Add a unique index on the FK fields, or
Configure Enrollment with a primary composite key similar to CourseAssignment . For more information, see
Indexes.
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
The preceding code adds the new entities and configures the CourseAssignment entity's composite PK.
Fluent API alternative to attributes
The OnModelCreating method in the preceding code uses the fluent API to configure EF Core behavior. The API is
called "fluent" because it's often used by stringing a series of method calls together into a single statement. The
following code is an example of the fluent API:
In this tutorial, the fluent API is used only for database mapping that can't be done with attributes. However, the
fluent API can specify most of the formatting, validation, and mapping rules that can be done with attributes.
Some attributes such as MinimumLength can't be applied with the fluent API. MinimumLength doesn't change the
schema, it only applies a minimum length validation rule.
Some developers prefer to use the fluent API exclusively so that they can keep their entity classes "clean."
Attributes and the fluent API can be mixed. There are some configurations that can only be done with the fluent
API (specifying a composite PK). There are some configurations that can only be done with attributes (
MinimumLength ). The recommended practice for using fluent API or attributes:
For more information about attributes vs. fluent API, see Methods of configuration.
Entity diagram
The following illustration shows the diagram that EF Power Tools create for the completed School model.
The preceding diagram shows:
Several one-to-many relationship lines (1 to *).
The one-to-zero-or-one relationship line (1 to 0..1) between the Instructor and OfficeAssignment entities.
The zero-or-one-to-many relationship line (0..1 to *) between the Instructor and Department entities.
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
//context.Database.EnsureCreated();
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}