0% found this document useful (0 votes)
1K views6,555 pages

ASP.NET Core Guide for Developers

This document provides an overview of ASP.NET Core, a cross-platform web application framework. It includes tutorials, documentation, and samples for creating different types of web applications, APIs, and real-time apps using features like MVC, Razor Pages, Blazor, and SignalR. It also covers core concepts, hosting, security, testing, performance, and deployment options to Azure, Linux, and Docker.

Uploaded by

erwinsambo46
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1K views6,555 pages

ASP.NET Core Guide for Developers

This document provides an overview of ASP.NET Core, a cross-platform web application framework. It includes tutorials, documentation, and samples for creating different types of web applications, APIs, and real-time apps using features like MVC, Razor Pages, Blazor, and SignalR. It also covers core concepts, hosting, security, testing, performance, and deployment options to Azure, Linux, and Docker.

Uploaded by

erwinsambo46
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 6555

Tell us about your PDF experience.

ASP.NET documentation
Learn to use ASP.NET Core to create web apps and services that are fast, secure, cross-platform,
and cloud-based. Browse tutorials, sample code, fundamentals, API reference and more.

GET STARTED W H AT ' S N E W


Create an ASP.NET Core app on Give feedback on ASP.NET
any platform in 5 minutes docs

OVERVIEW DOWNLOAD
ASP.NET Core overview Download .NET

GET STARTED GET STARTED


Create your first web UI Create your first web API

GET STARTED OVERVIEW


Create your first real-time web ASP.NET 4.x Documentation
app

Develop ASP.NET Core apps


Choose interactive web apps, web API, MVC-patterned apps, real-time apps, and more

Interactive client-side HTTP API apps Page-focused web UI


Blazor apps Develop HTTP services with with Razor Pages
Develop with reusable UI ASP.NET Core Develop page-focused web
components that can take apps with a clean separation of
advantage of WebAssembly for
b Create a minimal web API concerns
near-native performance with ASP.NET Core
d Create a web API with b Create your first Razor
e Overview ASP.NET Core Controllers Pages web app

b g g
b Build your first Blazor g Generate web API help g Create a page-focused web
app pages with Swagger / UI that consumes a web
b Build your first Blazor app OpenAPI API
with reusable components p Controller action return p Razor syntax
p Blazor hosting models types p Filters
p Format response data p Routing
p Handle errors
p Accessible ASP.NET Core
g Call an ASP.NET Core web web apps
API with JavaScript

Page-focused web UI Real-time web apps Remote Procedure Call


with MVC with SignalR (RPC) apps - gRPC
Develop web apps using the Add real-time functionality to services
Model-View-Controller design your web app, enable server- Develop contract-first, high-
pattern side code to push content performance services with
instantly gRPC in ASP.NET Core
e Overview
b Create your first ASP.NET e Overview e Overview
Core MVC app b Create your first SignalR b Create a gRPC client and
app server
p Views
g SignalR with Blazor p gRPC services concepts in
p Partial views
WebAssembly C#
p Controllers
g SignalR with TypeScript s Samples
p Routing to controller
s Samples p Compare gRPC services
actions
p Hubs with HTTP APIs
p Unit test
p SignalR client features g Add a gRPC service to an
ASP.NET Core app
p Host and scale
g Call gRPC services with the
.NET client
g Use gRPC in browser apps

Data-driven web apps Previous ASP.NET ASP.NET Core video


Create data-driven web apps in framework versions tutorials
ASP.NET Core Explore overviews, tutorials,
q ASP.NET Core 101 video
fundamental concepts,
g SQL with ASP.NET Core series
architecture and API reference
p Data binding in ASP.NET for previous ASP.NET… q Entity Framework Core 101
Core Blazor video series with .NET Core
p ASP.NET 4.x and ASP.NET Core
g SQL Server Express and
Razor Pages q Microservice architecture
with ASP.NET Core
g Entity Framework Core with
Razor Pages q Focus on Blazor video
series
g Entity Framework Core with
ASP.NET Core MVC q .NET Channel


g Azure Storage

g Blob Storage
p Azure Table Storage
p Microsoft Graph scenarios
for ASP.NET Core

Concepts and features

API reference for ASP.NET Core Servers


.NET API browser Overview
Kestrel
IIS
HTTP.sys

Host and deploy Security and identity


Overview Overview
Deploy to Azure App Service Authentication
DevOps for ASP.NET Core Developers Authorization
Linux with Apache Course: Secure an ASP.NET Core web app with the
Identity framework
Linux with Nginx
Data protection
Kestrel
Secrets management
IIS
Enforce HTTPS
HTTP.sys
Host Docker with HTTPS
Docker

Globalization and localization Test, debug and troubleshoot


Overview Razor Pages unit tests
Portable object localization Remote debugging
Localization extensibility Snapshot debugging
Troubleshoot Integration tests
Load and stress testing
Troubleshoot and debug
Logging
Load test Azure web apps by using Azure DevOps

Azure and ASP.NET Core Performance


Deploy an ASP.NET Core web app Overview
ASP.NET Core and Docker Memory and garbage collection
Host a web application with Azure App Service Response caching
App Service and Azure SQL Database Response compression
Managed identity with ASP.NET Core and Azure Diagnostic tools
SQL Database
Load and stress testing
Web API with CORS in Azure App Service
Capture Web Application Logs with App Service
Diagnostics Logging

Advanced features Migration


Model binding ASP.NET Core 5.0 to 6.0
Model validation ASP.NET Core 5.0 code samples to 6.0 minimal
hosting model
Write middleware
ASP.NET Core 3.1 to 5.0
Request and response operations
ASP.NET Core 3.0 to 3.1
URL rewriting
ASP.NET Core 2.2 to 3.0
ASP.NET Core 2.1 to 2.2
ASP.NET Core 2.0 to 2.1
ASP.NET Core 1.x to 2.0
ASP.NET to ASP.NET Core

Architecture
Choose between traditional web apps and Single
Page Apps (SPAs)
Architectural principles
Common web application architectures
Common client-side web technologies
Development process for Azure

Contribute to ASP.NET Core docs. Read our contributor guide .


Overview of ASP.NET Core
Article • 11/15/2022

By Daniel Roth , Rick Anderson , and Shaun Luttin

ASP.NET Core is a cross-platform, high-performance, open-source framework for


building modern, cloud-enabled, Internet-connected apps.

With ASP.NET Core, you can:

Build web apps and services, Internet of Things (IoT) apps, and mobile backends.
Use your favorite development tools on Windows, macOS, and Linux.
Deploy to the cloud or on-premises.
Run on .NET Core.

Why choose ASP.NET Core?


Millions of developers use or have used ASP.NET 4.x to create web apps. ASP.NET Core
is a redesign of ASP.NET 4.x, including architectural changes that result in a leaner, more
modular framework.

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.
Support for hosting Remote Procedure Call (RPC) services using gRPC.
A cloud-ready, environment-based configuration system.
Built-in dependency injection.
A lightweight, high-performance , and modular HTTP request pipeline.
Ability to host on the following:
Kestrel
IIS
HTTP.sys
Nginx
Apache
Docker
Side-by-side versioning.
Tooling that simplifies modern web development.

Build web APIs and web UI using ASP.NET Core


MVC
ASP.NET Core MVC provides features to build web APIs and web apps:

The Model-View-Controller (MVC) pattern helps make your web APIs and web
apps testable.
Razor Pages is a page-based programming model that makes building web UI
easier and more productive.
Razor markup provides a productive syntax for Razor Pages and MVC views.
Tag Helpers enable server-side code to participate in creating and rendering HTML
elements in Razor files.
Built-in support for multiple data formats and content negotiation lets your web
APIs reach a broad range of clients, including browsers and mobile devices.
Model binding automatically maps data from HTTP requests to action method
parameters.
Model validation automatically performs client-side and server-side validation.

Client-side development
ASP.NET Core integrates seamlessly with popular client-side frameworks and libraries,
including Blazor, Angular, React, and Bootstrap . For more information, see ASP.NET
Core Blazor and related topics under Client-side development.

ASP.NET Core target frameworks


ASP.NET Core 3.x or later can only target .NET Core. Generally, ASP.NET Core is
composed of .NET Standard libraries. Libraries written with .NET Standard 2.0 run on any
.NET platform that implements .NET Standard 2.0.

There are several advantages to targeting .NET Core, and these advantages increase
with each release. Some advantages of .NET Core over .NET Framework include:

Cross-platform. Runs on Windows, macOS, and Linux.


Improved performance
Side-by-side versioning
New APIs
Open source

Recommended learning path


We recommend the following sequence of tutorials for an introduction to developing
ASP.NET Core apps:

1. Follow a tutorial for the app type you want to develop or maintain.

App type Scenario Tutorial

Web app New server-side web UI development Get started with


Razor Pages

Web app Maintaining an MVC app Get started with MVC

Web app Client-side web UI development Get started with


Blazor

Web API RESTful HTTP services Create a web API†

Remote Contract-first services using Protocol Buffers Get started with a


Procedure Call gRPC service
app

Real-time app Bidirectional communication between servers Get started with


and connected clients SignalR

2. Follow a tutorial that shows how to do basic data access.

Scenario Tutorial

New development Razor Pages with Entity Framework Core

Maintaining an MVC app MVC with Entity Framework Core

3. Read an overview of ASP.NET Core fundamentals that apply to all app types.

4. Browse the table of contents for other topics of interest.

†There's also an interactive web API tutorial. No local installation of development tools is
required. The code runs in an Azure Cloud Shell in your browser, and curl is used for
testing.

Migrate from .NET Framework


For a reference guide to migrating ASP.NET 4.x apps to ASP.NET Core, see Update from
ASP.NET to ASP.NET Core.

How to download a sample


Many of the articles and tutorials include links to sample code.

1. Download the ASP.NET repository zip file .


2. Unzip the AspNetCore.Docs-main.zip file.
3. To access an article's sample app in the unzipped repository, use the URL in the
article's sample link to help you navigate to the sample's folder. Usually, an article's
sample link appears at the top of the article with the link text View or download
sample code.

Preprocessor directives in sample code


To demonstrate multiple scenarios, sample apps use the #define and #if-#else/#elif-
#endif preprocessor directives to selectively compile and run different sections of

sample code. For those samples that make use of this approach, set the #define
directive at the top of the C# files to define the symbol associated with the scenario that
you want to run. Some samples require defining the symbol at the top of multiple files
in order to run a scenario.

For example, the following #define symbol list indicates that four scenarios are available
(one scenario per symbol). The current sample configuration runs the TemplateCode
scenario:

C#

#define TemplateCode // or LogFromMain or ExpandDefault or FilterInCode

To change the sample to run the ExpandDefault scenario, define the ExpandDefault
symbol and leave the remaining symbols commented-out:

C#

#define ExpandDefault // TemplateCode or LogFromMain or FilterInCode

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#
directives. 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_WebHostDefaults :

C#

#region snippet_WebHostDefaults
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
#endregion

The preceding C# code snippet is referenced in the topic's markdown file with the
following line:

Markdown

[!code-csharp[](sample/SampleApp/Program.cs?name=snippet_WebHostDefaults)]

You may safely ignore (or remove) the #region and #endregion directives that surround
the code. Don't alter the code within these directives 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 .

Breaking changes and security advisories


Breaking changes and security advisories are reported on the Announcements repo .
Announcements can be limited to a specific version by selecting a Label filter.

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
Article • 04/11/2023

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.
Support for hosting Remote Procedure Call (RPC) services using gRPC.
A cloud-ready, environment-based configuration system.
Built-in dependency injection.
A lightweight, high-performance , and modular HTTP request pipeline.
Ability to host on the following:
Kestrel
IIS
HTTP.sys
Nginx
Apache
Docker
Side-by-side versioning.
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.

ASP.NET Core ASP.NET 4.x

Build for Windows, macOS, or Linux Build for Windows

Razor Pages is the recommended approach to create a Web Use Web Forms, SignalR, MVC,
UI as of ASP.NET Core 2.x. See also MVC, Web API, and Web API, WebHooks, or Web
SignalR. Pages

Multiple versions per machine One version per machine

Develop with Visual Studio , Visual Studio for Mac , or Develop with Visual Studio
Visual Studio Code using C# or F# using C#, VB, or F#

Higher performance than ASP.NET 4.x Good performance

Use .NET Core runtime Use .NET Framework runtime

See ASP.NET Core targeting .NET Framework for information on ASP.NET Core 2.x
support on .NET Framework.

ASP.NET Core scenarios


Websites
APIs
Real-time
Deploy an ASP.NET Core app to Azure

ASP.NET 4.x scenarios


Websites
APIs
Real-time
Create an ASP.NET 4.x web app in Azure

Additional resources
Introduction to ASP.NET
Introduction to ASP.NET Core
Deploy ASP.NET Core apps to Azure App Service
.NET vs. .NET Framework for server apps
Article • 10/04/2022

There are two supported .NET implementations for building server-side apps.

Implementation Included versions

.NET .NET Core 1.0 - 3.1, .NET 5, and later versions of .NET.

.NET Framework .NET Framework 1.0 - 4.8

Both share many of the same components, and you can share code across the two.
However, there are fundamental differences between the two, and your choice depends
on what you want to accomplish. This article provides guidance on when to use each.

Use .NET for your server application when:

You have cross-platform needs.


You're targeting microservices.
You're using Docker containers.
You need high-performance and scalable systems.
You need side-by-side .NET versions per application.

Use .NET Framework for your server application when:

Your app currently uses .NET Framework (recommendation is to extend instead of


migrating).
Your app uses third-party libraries or NuGet packages not available for .NET.
Your app uses .NET Framework technologies that aren't available for .NET.
Your app uses a platform that doesn't support .NET.

When to choose .NET


The following sections give a more detailed explanation of the previously stated reasons
for picking .NET over .NET Framework.

Cross-platform needs
If your web or service application needs to run on multiple platforms, for example,
Windows, Linux, and macOS, use .NET.
.NET supports the previously mentioned operating systems as your development
workstation. Visual Studio provides an Integrated Development Environment (IDE) for
Windows and macOS. You can also use Visual Studio Code, which runs on macOS, Linux,
and Windows. Visual Studio Code supports .NET, including IntelliSense and debugging.
Most third-party editors, such as Sublime, Emacs, and VI, work with .NET. These third-
party editors get editor IntelliSense using Omnisharp . You can also avoid any code
editor and directly use the .NET CLI, which is available for all supported platforms.

Microservices architecture
A microservices architecture allows a mix of technologies across a service boundary. This
technology mix enables a gradual embrace of .NET for new microservices that work with
other microservices or services. For example, you can mix microservices or services
developed with .NET Framework, Java, Ruby, or other monolithic technologies.

There are many infrastructure platforms available. Azure Service Fabric is designed for
large and complex microservice systems. Azure App Service is a good choice for
stateless microservices. Microservices alternatives based on Docker fit any microservices
approach, as explained in the Containers section. All these platforms support .NET and
make them ideal for hosting your microservices.

For more information about microservices architecture, see .NET Microservices.


Architecture for Containerized .NET Applications.

Containers
Containers are commonly used with a microservices architecture. Containers can also be
used to containerize web apps or services that follow any architectural pattern. .NET
Framework can be used on Windows containers. Still, the modularity and lightweight
nature of .NET make it a better choice for containers. When you're creating and
deploying a container, the size of its image is much smaller with .NET than with .NET
Framework. Because it's cross-platform, you can deploy server apps to Linux Docker
containers.

Docker containers can be hosted in your own Linux or Windows infrastructure or in a


cloud service such as Azure Kubernetes Service . Azure Kubernetes Service can
manage, orchestrate, and scale container-based applications in the cloud.

High-performance and scalable systems


When your system needs the best possible performance and scalability, .NET and
ASP.NET Core are your best options. The high-performance server runtime for Windows
Server and Linux makes ASP.NET Core a top-performing web framework on
TechEmpower benchmarks .

Performance and scalability are especially relevant for microservices architectures, where
hundreds of microservices might be running. With ASP.NET Core, systems run with a
much lower number of servers/Virtual Machines (VM). The reduced servers/VMs save
costs on infrastructure and hosting.

Side-by-side .NET versions per application level


To install applications with dependencies on different versions of .NET, we recommend
.NET. This implementation supports the side-by-side installation of different versions of
the .NET runtime on the same machine. The side-by-side installation allows multiple
services on the same server, each on its own version of .NET. It also lowers risks and
saves money in application upgrades and IT operations.

Side-by-side installation isn't possible with .NET Framework. It's a Windows component,
and only one version can exist on a machine at a time. Each version of .NET Framework
replaces the previous version. If you install a new app that targets a later version of .NET
Framework, you might break existing apps that run on the machine because the
previous version was replaced.

When to choose .NET Framework


.NET offers significant benefits for new applications and application patterns. However,
.NET Framework continues to be the natural choice for many existing scenarios, and as
such, .NET Framework isn't replaced by .NET for all server applications.

Current .NET Framework applications


In most cases, you don't need to migrate your existing applications to .NET. Instead, we
recommend using .NET as you extend an existing application, such as writing a new web
service in ASP.NET Core.

Third-party libraries or NuGet packages not available for


.NET
.NET Standard enables sharing code across all .NET implementations, including .NET
Core/5+. With .NET Standard 2.0, a compatibility mode allows .NET Standard and .NET
projects to reference .NET Framework libraries. For more information, see Support for
.NET Framework libraries.

You need to use .NET Framework only in cases where the libraries or NuGet packages
use technologies that aren't available in .NET Standard or .NET.

.NET Framework technologies not available for .NET


Some .NET Framework technologies aren't available in .NET. The following list shows the
most common technologies not found in .NET:

ASP.NET Web Forms applications: ASP.NET Web Forms are only available in .NET
Framework. ASP.NET Core can't be used for ASP.NET Web Forms.

ASP.NET Web Pages applications: ASP.NET Web Pages aren't included in ASP.NET
Core.

Workflow-related services: Windows Workflow Foundation (WF), Workflow


Services (WCF + WF in a single service), and WCF Data Services (formerly known as
"ADO.NET Data Services") are only available in .NET Framework.

Language support: Visual Basic and F# are currently supported in .NET but not for
all project types. For a list of supported project templates, see Template options for
dotnet new.

For more information, see .NET Framework technologies unavailable in .NET.

Platform doesn't support .NET


Some Microsoft or third-party platforms don't support .NET. Some Azure services
provide an SDK not yet available for consumption on .NET. In such cases, you can use
the equivalent REST API instead of the client SDK.

See also
Choose between ASP.NET and ASP.NET Core
ASP.NET Core targeting .NET Framework
Target frameworks
.NET introduction
Porting from .NET Framework to .NET 5
Introduction to .NET and Docker
.NET implementations
.NET Microservices. Architecture for Containerized .NET Applications
Tutorial: Get started with ASP.NET Core
Article • 07/22/2022

This tutorial shows how to create and run an ASP.NET Core web app using the .NET Core
CLI.

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 7.0 SDK

Create a web app project


Open a command shell, and enter the following command:

.NET CLI

dotnet new webapp -o aspnetcoreapp


The preceding command:

Creates a new web app.


The -o aspnetcoreapp parameter creates a directory named aspnetcoreapp with
the source files for the app.

Trust the development certificate


Trust the HTTPS development certificate:

Windows

.NET CLI

dotnet dev-certs https --trust

The preceding command displays the following dialog:

Select Yes if you agree to trust the development certificate.

For more information, see Trust the ASP.NET Core HTTPS development certificate

Run the app


Run the following commands:

.NET CLI

cd aspnetcoreapp
dotnet watch run

After the command shell indicates that the app has started, browse to
https://localhost:{port} , where {port} is the random port used.

Edit a Razor page


Open Pages/Index.cshtml and modify and save the page with the following highlighted
markup:

CSHTML

@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>

Browse to https://localhost:{port} , refresh the page, and verify the changes are
displayed.

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 following:

Overview of ASP.NET Core


ASP.NET Core documentation - what's
new?
Welcome to what's new in ASP.NET Core docs. Use this page to quickly find the latest
changes.

Find ASP.NET Core docs updates

h WHAT'S NEW

May 2023

April 2023

March 2023

February 2023

January 2023

December 2022

Get involved - contribute to ASP.NET Core docs

e OVERVIEW

ASP.NET Core docs repository

Project structure and labels for issues and pull requests

p CONCEPT

Contributor guide

ASP.NET Core docs contributor guide

ASP.NET Core API reference docs contributor guide

Community

h WHAT'S NEW

Community
Community

Related what's new pages

h WHAT'S NEW

Xamarin docs updates

.NET Core release notes

ASP.NET Core release notes

C# compiler (Roslyn) release notes

Visual Studio release notes

Visual Studio for Mac release notes

Visual Studio Code release notes


What's new in ASP.NET Core 8.0
Article • 06/22/2023

This article highlights the most significant changes in ASP.NET Core 8.0 with links to
relevant documentation.

This article is under development and not complete. More information may be found in
the ASP.NET Core 8.0 preview blogs and GitHub issue:

ASP.NET Core roadmap for .NET 8 on GitHub


What's new in .NET 8 Preview 1
What's new in .NET 8 Preview 2
What's new in .NET 8 Preview 3
What's new in .NET 8 Preview 4
What's new in .NET 8 Preview 5

SignalR

New approach to set the server timeout and Keep-Alive


interval
ServerTimeout (default: 30 seconds) and KeepAliveInterval (default: 15 seconds) can be
set directly on HubConnectionBuilder.

Prior approach for JavaScript clients

The following example shows the assignment of values that are double the default
values in ASP.NET Core 7.0 or earlier:

JavaScript

var connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.build();

connection.serverTimeoutInMilliseconds = 60000;
connection.keepAliveIntervalInMilliseconds = 30000;

New approach for JavaScript clients


The following example shows the new approach for assigning values that are double the
default values in ASP.NET Core 8.0 or later:

JavaScript

var connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.withServerTimeoutInMilliseconds(60000)
.withKeepAliveIntervalInMilliseconds(30000)
.build();

Prior approach for the JavaScript client of a Blazor Server app

The following example shows the assignment of values that are double the default
values in ASP.NET Core 7.0 or earlier:

JavaScript

Blazor.start({
configureSignalR: function (builder) {
let c = builder.build();
c.serverTimeoutInMilliseconds = 60000;
c.keepAliveIntervalInMilliseconds = 30000;
builder.build = () => {
return c;
};
}
});

New approach for the JavaScript client of a Blazor Server app


The following example shows the new approach for assigning values that are double the
default values in ASP.NET Core 8.0 or later:

JavaScript

Blazor.start({
configureSignalR: function (builder) {
builder.withServerTimeout(60000).withKeepAliveInterval(30000);
}
});

Prior approach for .NET clients


The following example shows the assignment of values that are double the default
values in ASP.NET Core 7.0 or earlier:

C#

var builder = new HubConnectionBuilder()


.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.Build();

builder.ServerTimeout = TimeSpan.FromSeconds(60);
builder.KeepAliveInterval = TimeSpan.FromSeconds(30);

builder.On<string, string>("ReceiveMessage", (user, message) => ...

await builder.StartAsync();

New approach for .NET clients


The following example shows the new approach for assigning values that are double the
default values in ASP.NET Core 8.0 or later:

C#

var builder = new HubConnectionBuilder()


.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.WithServerTimeout(TimeSpan.FromSeconds(60))
.WithKeepAliveInterval(TimeSpan.FromSeconds(30))
.Build();

builder.On<string, string>("ReceiveMessage", (user, message) => ...

await builder.StartAsync();

Minimal APIs
This section describes new features for minimal APIs. See also the section on native AOT
for more information relevant to minimal APIs.

Binding to forms
Explicit binding to form values using the [FromForm] attribute is now supported.
Inferred binding to forms using the IFormCollection, IFormFile, and IFormFileCollection
types is also supported. OpenAPI metadata is inferred for form parameters to support
integration with Swagger UI.
For more information, see:

Explicit binding from form values.


Binding to forms with IFormCollection, IFormFile, and IFormFileCollection.

Support for AsParameters and automatic metadata


generation
Minimal APIs generated at compile-time include support for parameters decorated with
the [AsParameters] attribute and support automatic metadata inference for request and
response types. Consider the following code:

C#

using System.Text.Json.Serialization;
using MyFirstAotWebApi;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0,
AppJsonSerializerContext.Default);
});

var app = builder.Build();

var sampleTodos = TodoGenerator.GenerateTodos().ToArray();

var todosApi = app.MapGroup("/todos");


todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
? Results.Ok(todo)
: Results.NotFound());

app.MapPost("/todos", ([AsParameters] CreateTodoArgs payload) =>


{
if (payload.TodoToCreate is not null)
{
return payload.TodoToCreate;
}
return new Todo(0, "New todo", DateTime.Now, false);
});

app.Run();

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

record CreateTodoArgs(int ProjectId, Todo? TodoToCreate);


record Todo(int Id, string Name, DateTime CreatedAt, bool IsCompleted);

The preceding generated code:

Binds a projectId parameter from the query.


Binds a Todo parameter from the JSON body.
Annotates the endpoint metadata to indicate that it accepts a JSON payload.
Annotate the endpoint metadata to indicate that it returns a Todo as a JSON
payload.

Native AOT
Support for .NET native ahead-of-time (AOT) has been added. Apps that are published
using AOT can have substantially better performance: smaller app size, less memory
usage, and faster startup time. Native AOT is currently supported by gRPC, minimal API,
and worker service apps. For more information, see ASP.NET Core support for native
AOT and Tutorial: Publish an ASP.NET Core app using native AOT. For information about
known issues with ASP.NET Core and native AOT compatibility, see GitHub issue
dotnet/core #8288 .

New project template


The new ASP.NET Core API template in Visual Studio 2022 has an Enable native AOT
publish option. The equivalent template and option in the CLI is the dotnet new api
command and the --aot option. This template is intended to produce a project focused
on cloud-native, API-first scenarios. For more information, see The API template.

New CreateSlimBuilder method


The CreateSlimBuilder() method used in the API template initializes the
WebApplicationBuilder with the minimum ASP.NET Core features necessary to run an
app. It's used by the API template whether or not the AOT option is used. For more
information, see The CreateSlimBuilder method.

Reduced app size with configurable HTTPS support


We've further reduced native AOT binary size for apps that don't need HTTPS or HTTP/3
support. Not using HTTPS or HTTP/3 is common for apps that run behind a TLS
termination proxy (for example, hosted on Azure). The new
WebApplication.CreateSlimBuilder method omits this functionality by default. It can be
added by calling builder.WebHost.UseKestrelHttpsConfiguration() for HTTPS or
builder.WebHost.UseQuic() for HTTP/3. For more information, see The CreateSlimBuilder
method.

JSON serialization of compiler-generated


IAsyncEnumerable<T> types

New features were added to System.Text.Json to better support native AOT. These new
features add capabilities for the source generation mode of System.Text.Json , because
reflection isn't supported by AOT.

One of the new features is support for JSON serialization of IAsyncEnumerable<T>


implementations implemented by the C# compiler. This support opens up their use in
ASP.NET Core projects configured to publish native AOT.

This API is useful in scenarios where a route handler uses yield return to
asynchronously return an enumeration. For example, to materialize rows from a
database query. For more information, see Unspeakable type support in the .NET 8
Preview 4 announcement.

For information abut other improvements in System.Text.Json source generation, see


Serialization improvements in .NET 8.

Top-level APIs annotated for trim warnings


The main entry points to subsystems that don't work reliably with native AOT are now
annotated. When these methods are called from an application with native AOT
enabled, a warning is provided. For example, the following code produces a warning at
the invocation of AddControllers because this API isn't trim-safe and isn't supported by
native AOT.
Request delegate generator
In order to make Minimal APIs compatible with native AOT, we're introducing the
Request Delegate Generator (RDG). The RDG is a source generator that does what the
RequestDelegateFactory (RDF) does. That is, it turns the various MapGet() , MapPost() ,
and calls like them into RequestDelegate instances associated with the specified routes.
But rather than doing it in-memory in an application when it starts, the RDG does it at
compile time and generates C# code directly into the project. The RDG:

Removes the runtime generation of this code.


Ensures that the types used in APIs are statically analyzable by the native AOT tool-
chain.
Ensures that required code is not trimmed away.

We're working to ensure that as many as possible of the Minimal API features are
supported by the RDG and thus compatible with native AOT.

The RDG is enabled automatically in a project when publishing with native AOT is
enabled. RDG can be manually enabled even when not using native AOT by setting
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator> in the project
file. This can be useful when initially evaluating a project's readiness for native AOT, or to
reduce the startup time of an app.

Logging and exception handling in compile-time


generated minimal APIs
Minimal APIs generated at run time support automatically logging (or throwing
exceptions in Development environments) when parameter binding fails. .NET 8
introduces the same support for APIs generated at compile time via the Request
Delegate Generator (RDG). For more information, see Logging and exception handling
in compile-time generated minimal APIs .

AOT and System.Text.Json


Minimal APIs are optimized for receiving and returning JSON payloads using
System.Text.Json , so the compatibility requirements for JSON and native AOT apply

too. Native AOT compatibility requires the use of the System.Text.Json source
generator. All types accepted as parameters to or returned from request delegates in
Minimal APIs must be configured on a JsonSerializerContext that is registered via
ASP.NET Core's dependency injection, for example:

C#

// Register the JSON serializer context with DI


builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0,
AppJsonSerializerContext.Default);
});

...

// Add types used in the minimal API app to source generated JSON serializer
content
[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{

For more information about the TypeInfoResolverChain API, see the following resources:

JsonSerializerOptions.TypeInfoResolverChain
Chain source generators
Changes to support source generation

Libraries and native AOT


Many of the common libraries available for ASP.NET Core projects today have some
compatibility issues if used in a project targeting native AOT. Popular libraries often rely
on the dynamic capabilities of .NET reflection to inspect and discover types,
conditionally load libraries at runtime, and generate code on the fly to implement their
functionality. These libraries need to be updated in order to work with native AOT by
using tools like Roslyn source generators.

Library authors wishing to learn more about preparing their libraries for native AOT are
encouraged to start by preparing their library for trimming and learning more about the
native AOT compatibility requirements.

Kestrel and HTTP.sys servers


There are several new features for Kestrel and HTTP.sys.

Support for named pipes in Kestrel


Named pipes is a popular technology for building inter-process communication (IPC)
between Windows apps. You can now build an IPC server using .NET, Kestrel, and named
pipes.

C#

var builder = WebApplication.CreateBuilder(args);


builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.ListenNamedPipe("MyPipeName");
});

For more information about this feature and how to use .NET and gRPC to create an IPC
server and client, see Inter-process communication with gRPC.

HTTP/3 enabled by default in Kestrel


HTTP/3 is a new internet technology that was standardized in June 2022. HTTP/3 offers
several advantages over older HTTP protocols, including:

Faster connection setup.


No head-of-line blocking.
Better transitions between networks.

.NET 7 added support for HTTP/3 to ASP.NET Core and Kestrel. ASP.NET Core apps
could choose to turn it on. In .NET 8, HTTP/3 is enabled by default for Kestrel, alongside
HTTP/1.1 and HTTP/2. For more information about HTTP/3 and its requirements, see
Use HTTP/3 with the ASP.NET Core Kestrel web server.
HTTP/2 over TLS (HTTPS) support on macOS in Kestrel
.NET 8 adds support for Application-Layer Protocol Negotiation (ALPN) to macOS. ALPN
is a TLS feature used to negotiate which HTTP protocol a connection will use. For
example, ALPN allows browsers and other HTTP clients to request an HTTP/2
connection. This feature is especially useful for gRPC apps, which require HTTP/2. For
more information, see Use HTTP/2 with the ASP.NET Core Kestrel web server.

Warning when specified HTTP protocols won't be used


If TLS is disabled and HTTP/1.x is available, HTTP/2 and HTTP/3 will be disabled, even if
they've been specified. This can cause some nasty surprises, so we've added warning
output to let you know when it happens.

HTTP_PORTS and HTTPS_PORTS config keys

Applications and containers are often only given a port to listen on, like 80, without
additional constraints like host or path. HTTP_PORTS and HTTPS_PORTS are new config
keys that allow specifying the listening ports for the Kestrel and HTTP.sys servers. These
may be defined with the DOTNET_ or ASPNETCORE_ environment variable prefixes, or
specified directly through any other config input like appsettings.json. Each is a
semicolon delimited list of port values. For example:

cli

ASPNETCORE_HTTP_PORTS=80;8080
ASPNETCORE_HTTPS_PORTS=443;8081

This is shorthand for the following, which specifies the scheme (HTTP or HTTPS) and any
host or IP:

cli

ASPNETCORE_URLS=http://*:80/;http://*:8080/;https://*:443/;https://*:8081/

For more information, see Configure endpoints for the ASP.NET Core Kestrel web server
and HTTP.sys web server implementation in ASP.NET Core.

IHttpSysRequestTimingFeature
IHttpSysRequestTimingFeature provides detailed timing information for requests
when using the HTTP.sys server and In-process hosting with IIS:

Timestamps are obtained using QueryPerformanceCounter.


The timestamp frequency can be obtained via QueryPerformanceFrequency.
The index of the timing can be cast to HttpSysRequestTimingType to know what
the timing represents.
The value may be 0 if the timing isn't available for the current request.

IHttpSysRequestTimingFeature.TryGetTimestamp retrieves the timestamp for the


provided timing type:

C#

using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>


{
var feature =
context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();

var loggerFactory =
context.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Sample");

var timingType = HttpSysRequestTimingType.RequestRoutingEnd;

if (feature.TryGetTimestamp(timingType, out var timestamp))


{
logger.LogInformation("Timestamp {timingType}: {timestamp}",
timingType, timestamp);
}
else
{
logger.LogInformation("Timestamp {timingType}: not available for the
"
+ "current request",
timingType);
}

return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();
For more information, see Get detailed timing information with
IHttpSysRequestTimingFeature and Timing information and In-process hosting with IIS.

Authentication and authorization


ASP.NET Core 8 adds new features to authentication and authorization.

Identity API endpoints


MapIdentityApi<TUser>() is a new extension method that adds two API endpoints
( /register and /login ). The main goal of the MapIdentityApi is to make it easy for
developers to use ASP.NET Core Identity for authentication in JavaScript-based single
page apps (SPA) or Blazor apps. Instead of using the default UI provided by ASP.NET
Core Identity, which is based on Razor Pages, MapIdentityApi adds JSON API endpoints
that are more suitable for SPA apps and non-browser apps. For more information, see
Identity API endpoints .

IAuthorizationRequirementData
Prior to ASP.NET Core 8, adding a parameterized authorization policy to an endpoint
required implementing an:

AuthorizeAttribute for each policy.

AuthorizationPolicyProvider to process a custom policy from a string-based


contract.
AuthorizationRequirement for the policy.
AuthorizationHandler for each requirement.

For example, consider the following sample written for ASP.NET Core 7.0:

C#

using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();

var app = builder.Build();

app.MapControllers();

app.Run();

C#

using Microsoft.AspNetCore.Mvc;

namespace AuthRequirementsData.Controllers;

[ApiController]
[Route("api/[controller]")]
public class GreetingsController : Controller
{
[MinimumAgeAuthorize(16)]
[HttpGet("hello")]
public string Hello() => $"Hello {(HttpContext.User.Identity?.Name ??
"world")}!";
}

C#

using Microsoft.AspNetCore.Authorization;
using System.Globalization;
using System.Security.Claims;

namespace AuthRequirementsData.Authorization;

class MinimumAgeAuthorizationHandler :
AuthorizationHandler<MinimumAgeRequirement>
{
private readonly ILogger<MinimumAgeAuthorizationHandler> _logger;

public
MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler>
logger)
{
_logger = logger;
}

// Check whether a given MinimumAgeRequirement is satisfied or not for a


particular
// context.
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
MinimumAgeRequirement
requirement)
{
// Log as a warning so that it's very clear in sample output which
authorization
// policies(and requirements/handlers) are in use.
_logger.LogWarning("Evaluating authorization requirement for age >=
{age}",

requirement.Age);

// Check the user's age


var dateOfBirthClaim = context.User.FindFirst(c => c.Type ==

ClaimTypes.DateOfBirth);
if (dateOfBirthClaim != null)
{
// If the user has a date of birth claim, check their age
var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value,
CultureInfo.InvariantCulture);
var age = DateTime.Now.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Now.AddYears(-age))
{
// Adjust age if the user hasn't had a birthday yet this
year.
age--;
}

// If the user meets the age criterion, mark the authorization


requirement
// succeeded.
if (age >= requirement.Age)
{
_logger.LogInformation("Minimum age authorization
requirement {age} satisfied",
requirement.Age);
context.Succeed(requirement);
}
else
{
_logger.LogInformation("Current user's DateOfBirth claim
({dateOfBirth})" +
" does not satisfy the minimum age authorization
requirement {age}",
dateOfBirthClaim.Value,
requirement.Age);
}
}
else
{
_logger.LogInformation("No DateOfBirth claim present");
}

return Task.CompletedTask;
}
}
The complete sample is here in the AspNetCore.Docs.Samples repository.

ASP.NET Core 8 introduces the IAuthorizationRequirementData interface. The


IAuthorizationRequirementData interface allows the attribute definition to specify the

requirements associated with the authorization policy. Using


IAuthorizationRequirementData , the preceding custom authorization policy code can be

written with fewer lines of code. The updated Program.cs file:

diff

using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
- builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();

var app = builder.Build();

app.MapControllers();

app.Run();

The updated MinimumAgeAuthorizationHandler :

diff

using Microsoft.AspNetCore.Authorization;
using System.Globalization;
using System.Security.Claims;

namespace AuthRequirementsData.Authorization;

- class MinimumAgeAuthorizationHandler :
AuthorizationHandler<MinimumAgeRequirement>
+ class MinimumAgeAuthorizationHandler :
AuthorizationHandler<MinimumAgeAuthorizeAttribute>
{
private readonly ILogger<MinimumAgeAuthorizationHandler> _logger;

public
MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler>
logger)
{
_logger = logger;
}

// Check whether a given MinimumAgeRequirement is satisfied or not for a


particular
// context
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
- MinimumAgeRequirement
requirement)
+ MinimumAgeAuthorizeAttribute
requirement)
{
// Remaining code omitted for brevity.

The complete updated sample can be found here .

See Custom authorization policies with IAuthorizationRequirementData for a detailed


examination of the new sample.

Miscellaneous
The following sections describe miscellaneous new features in ASP.NET Core 8.

Support for generic attributes


Attributes that previously required a Type parameter are now available in cleaner
generic variants. This is made possible by support for generic attributes in C# 11. For
example, the syntax for annotating the response type of an action can be modified as
follows:

diff

[ApiController]
[Route("api/[controller]")]
public class TodosController : Controller
{
[HttpGet("/")]
- [ProducesResponseType(typeof(Todo), StatusCodes.Status200OK)]
+ [ProducesResponseType<Todo>(StatusCodes.Status200OK)]
public Todo Get() => new Todo(1, "Write a sample", DateTime.Now, false);
}

Generic variants are supported for the following attributes:

[ProducesResponseType<T>]
[Produces<T>]

[MiddlewareFilter<T>]
[ModelBinder<T>]

[ModelMetadataType<T>]
[ServiceFilter<T>]

[TypeFilter<T>]

Code analysis in ASP.NET Core apps


The new analyzers shown in the following table are available in ASP.NET Core 8.0.

Diagnostic Breaking or non- Description


ID breaking

ASP0016 Non-breaking Do not return a value from RequestDelegate

ASP0019 Non-breaking Suggest using IHeaderDictionary.Append or the indexer

ASP0020 Non-breaking Complex types referenced by route parameters must be


parsable

ASP0021 Non-breaking The return type of the BindAsync method must be


ValueTask<T>

ASP0022 Non-breaking Route conflict detected between route handlers

ASP0023 Non-breaking MVC: Route conflict detected between route handlers

ASP0024 Non-breaking Route handler has multiple parameters with the


[FromBody] attribute

ASP0025 Non-breaking Use AddAuthorizationBuilder

Route tooling
ASP.NET Core is built on routing. Minimal APIs, Web APIs, Razor Pages, and Blazor all
use routes to customize how HTTP requests map to code.

In .NET 8 we've invested in a suite of new features to make routing easier to learn and
use. These new features include:

Route syntax highlighting


Autocomplete of parameter and route names
Autocomplete of route constraints
Route analyzers and fixers
Route syntax analyzer
Mismatched parameter optionality analyzer and fixer
Ambiguous Minimal API and Web API route analyzer
Support for Minimal APIs, Web APIs, and Blazor

For more information, see Route tooling in .NET 8 .

ASP.NET Core metrics


Metrics are measurements reported over time and are most often used to monitor the
health of an app and to generate alerts. For example, a counter that reports failed HTTP
requests could be displayed in dashboards or generate alerts when failures pass a
threshold.

This preview adds new metrics throughout ASP.NET Core using


System.Diagnostics.Metrics. Metrics is a modern API for reporting and collecting
information about apps.

Metrics offers a number of improvements compared to existing event counters:

New kinds of measurements with counters, gauges and histograms.


Powerful reporting with multi-dimensional values.
Integration into the wider cloud native ecosystem by aligning with OpenTelemetry
standards.

Metrics have been added for ASP.NET Core hosting, Kestrel, and SignalR. For more
information, see System.Diagnostics.Metrics.
What's new in ASP.NET Core 7.0
Article • 03/13/2023

This article highlights the most significant changes in ASP.NET Core 7.0 with links to
relevant documentation.

Rate limiting middleware in ASP.NET Core


The Microsoft.AspNetCore.RateLimiting middleware provides rate limiting middleware.
Apps configure rate limiting policies and then attach the policies to endpoints. For more
information, see Rate limiting middleware in ASP.NET Core.

Authentication uses single scheme as


DefaultScheme
As part of the work to simplify authentication, when there's only a single authentication
scheme registered, it's automatically used as the DefaultScheme and doesn't need to be
specified. For more information, see DefaultScheme.

MVC and Razor pages

Support for nullable models in MVC views and Razor


Pages
Nullable page or view models are supported to improve the experience when using null
state checking with ASP.NET Core apps:

C#

@model Product?

Bind with IParsable<T>.TryParse in MVC and API


Controllers
The IParsable<TSelf>.TryParse API supports binding controller action parameter values.
For more information, see Bind with IParsable<T>.TryParse.
Customize the cookie consent value
In ASP.NET Core versions earlier than 7, the cookie consent validation uses the cookie
value yes to indicate consent. Now you can specify the value that represents consent.
For example, you could use true instead of yes :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.ConsentCookieValue = "true";
});

var app = builder.Build();

For more information, see Customize the cookie consent value.

API controllers

Parameter binding with DI in API controllers


Parameter binding for API controller actions binds parameters through dependency
injection when the type is configured as a service. This means it's no longer required to
explicitly apply the [FromServices] attribute to a parameter. In the following code, both
actions return the time:

C#

[Route("[controller]")]
[ApiController]
public class MyController : ControllerBase
{
public ActionResult GetWithAttribute([FromServices] IDateTime dateTime)
=> Ok(dateTime.Now);

[Route("noAttribute")]
public ActionResult Get(IDateTime dateTime) => Ok(dateTime.Now);
}
In rare cases, automatic DI can break apps that have a type in DI that is also accepted in
an API controllers action method. It's not common to have a type in DI and as an
argument in an API controller action. To disable automatic binding of parameters, set
DisableImplicitFromServicesParameters

C#

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.DisableImplicitFromServicesParameters = true;
});

var app = builder.Build();

app.MapControllers();

app.Run();

In ASP.NET Core 7.0, types in DI are checked at app startup with


IServiceProviderIsService to determine if an argument in an API controller action comes
from DI or from the other sources.

The new mechanism to infer binding source of API Controller action parameters uses
the following rules:

1. A previously specified BindingInfo.BindingSource is never overwritten.


2. A complex type parameter, registered in the DI container, is assigned
BindingSource.Services.
3. A complex type parameter, not registered in the DI container, is assigned
BindingSource.Body.
4. A parameter with a name that appears as a route value in any route template is
assigned BindingSource.Path.
5. All other parameters are BindingSource.Query.

JSON property names in validation errors


By default, when a validation error occurs, model validation produces a
ModelStateDictionary with the property name as the error key. Some apps, such as
single page apps, benefit from using JSON property names for validation errors
generated from Web APIs. The following code configures validation to use the
SystemTextJsonValidationMetadataProvider to use JSON property names:

C#

using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
options.ModelMetadataDetailsProviders.Add(new
SystemTextJsonValidationMetadataProvider());
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

The following code configures validation to use the


NewtonsoftJsonValidationMetadataProvider to use JSON property name when using
Json.NET :

C#

using Microsoft.AspNetCore.Mvc.NewtonsoftJson;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
options.ModelMetadataDetailsProviders.Add(new
NewtonsoftJsonValidationMetadataProvider());
}).AddNewtonsoftJson();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
For more information, see Use JSON property names in validation errors

Minimal APIs

Filters in Minimal API apps


Minimal API filters allow developers to implement business logic that supports:

Running code before and after the route handler.


Inspecting and modifying parameters provided during a route handler invocation.
Intercepting the response behavior of a route handler.

Filters can be helpful in the following scenarios:

Validating the request parameters and body that are sent to an endpoint.
Logging information about the request and response.
Validating that a request is targeting a supported API version.

For more information, see Filters in Minimal API apps

Bind arrays and string values from headers and query


strings
In ASP.NET 7, binding query strings to an array of primitive types, string arrays, and
StringValues is supported:

C#

// Bind query string values to a primitive type array.


// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.


// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

Binding query strings or header values to an array of complex types is supported when
the type has TryParse implemented. For more information, see Bind arrays and string
values from headers and query strings.

For more information, see Add endpoint summary or description.

Bind the request body as a Stream or PipeReader


The request body can bind as a Stream or PipeReader to efficiently support scenarios
where the user has to process data and:

Store the data to blob storage or enqueue the data to a queue provider.
Process the stored data with a worker process or cloud function.

For example, the data might be enqueued to Azure Queue storage or stored in Azure
Blob storage.

For more information, see Bind the request body as a Stream or PipeReader

New Results.Stream overloads


We introduced new Results.Stream overloads to accommodate scenarios that need
access to the underlying HTTP response stream without buffering. These overloads also
improve cases where an API streams data to the HTTP response stream, like from Azure
Blob Storage. The following example uses ImageSharp to return a reduced size of the
specified image:

C#

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http,


CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age=
{TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream,
token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream,


CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken:
token);
}

For more information, see Stream examples

Typed results for minimal APIs


In .NET 6, the IResult interface was introduced to represent values returned from
minimal APIs that don't utilize the implicit support for JSON serializing the returned
object to the HTTP response. The static Results class is used to create varying IResult
objects that represent different types of responses. For example, setting the response
status code or redirecting to another URL. The IResult implementing framework types
returned from these methods were internal however, making it difficult to verify the
specific IResult type being returned from methods in a unit test.

In .NET 7 the types implementing IResult are public, allowing for type assertions when
testing. For example:

C#

[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}

Improved unit testability for minimal route handlers


IResult implementation types are now publicly available in the
Microsoft.AspNetCore.Http.HttpResults namespace. The IResult implementation types
can be used to unit test minimal route handlers when using named methods instead of
lambdas.

The following code uses the Ok<TValue> class:


C#

[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();

context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title",
Description = "Test description",
IsDone = false
});

await context.SaveChangesAsync();

// Act
var result = await TodoEndpointsV1.GetTodo(1, context);

//Assert
Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

var okResult = (Ok<Todo>)result.Result;

Assert.NotNull(okResult.Value);
Assert.Equal(1, okResult.Value.Id);
}

For more information, see IResult implementation types.

New HttpResult interfaces


The following interfaces in the Microsoft.AspNetCore.Http namespace provide a way to
detect the IResult type at runtime, which is a common pattern in filter
implementations:

IContentTypeHttpResult
IFileHttpResult
INestedHttpResult
IStatusCodeHttpResult
IValueHttpResult
IValueHttpResult<TValue>

For more information, see IHttpResult interfaces.

OpenAPI improvements for minimal APIs


Microsoft.AspNetCore.OpenApi NuGet package

The Microsoft.AspNetCore.OpenApi package allows interactions with OpenAPI


specifications for endpoints. The package acts as a link between the OpenAPI models
that are defined in the Microsoft.AspNetCore.OpenApi package and the endpoints that
are defined in Minimal APIs. The package provides an API that examines an endpoint's
parameters, responses, and metadata to construct an OpenAPI annotation type that is
used to describe an endpoint.

C#

app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>


{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();

return Results.Created($"/todoitems/{todo.Id}", todo);


})
.WithOpenApi();

Call WithOpenApi with parameters


The WithOpenApi method accepts a function that can be used to modify the OpenAPI
annotation. For example, in the following code, a description is added to the first
parameter of the endpoint:

C#

app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>


{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();

return Results.Created($"/todoitems/{todo.Id}", todo);


})
.WithOpenApi(generatedOperation =>
{
var parameter = generatedOperation.Parameters[0];
parameter.Description = "The ID associated with the created Todo";
return generatedOperation;
});

Provide endpoint descriptions and summaries


Minimal APIs now support annotating operations with descriptions and summaries for
OpenAPI spec generation. You can call extension methods WithDescription and
WithSummary or use attributes [EndpointDescription] and [EndpointSummary]).

For more information, see OpenAPI in minimal API apps

File uploads using IFormFile and IFormFileCollection


Minimal APIs now support file upload with IFormFile and IFormFileCollection . The
following code uses IFormFile and IFormFileCollection to upload file:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>


{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>


{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});

app.Run();

Authenticated file upload requests are supported using an Authorization header , a


client certificate, or a cookie header.

There is no built-in support for antiforgery. However, it can be implemented using the
IAntiforgery service.

[AsParameters] attribute enables parameter binding for


argument lists
The [AsParameters] attribute enables parameter binding for argument lists. For more
information, see Parameter binding for argument lists with [AsParameters].

Minimal APIs and API controllers

New problem details service


The problem details service implements the IProblemDetailsService interface, which
supports creating Problem Details for HTTP APIs .

For more information, see Problem details service.

Route groups
The MapGroup extension method helps organize groups of endpoints with a common
prefix. It reduces repetitive code and allows for customizing entire groups of endpoints
with a single call to methods like RequireAuthorization and WithMetadata which add
endpoint metadata.

For example, the following code creates two similar groups of endpoints:

C#

app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");

app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();

EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext
factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;

foreach (var argument in factoryContext.MethodInfo.GetParameters())


{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}

return async invocationContext =>


{
var dbContext = invocationContext.GetArgument<TodoDb>
(dbContextIndex);
dbContext.IsPrivate = true;

try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise
reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}

C#

public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)


{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);

return group;
}

In this scenario, you can use a relative address for the Location header in the 201
Created result:

C#

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb


database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}

The first group of endpoints will only match requests prefixed with /public/todos and
are accessible without any authentication. The second group of endpoints will only
match requests prefixed with /private/todos and require authentication.

The QueryPrivateTodos endpoint filter factory is a local function that modifies the route
handler's TodoDb parameters to allow to access and store private todo data.

Route groups also support nested groups and complex prefix patterns with route
parameters and constraints. In the following example, and route handler mapped to the
user group can capture the {org} and {group} route parameters defined in the outer

group prefixes.

The prefix can also be empty. This can be useful for adding endpoint metadata or filters
to a group of endpoints without changing the route pattern.

C#

var all = app.MapGroup("").WithOpenApi();


var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

Adding filters or metadata to a group behaves the same way as adding them
individually to each endpoint before adding any extra filters or metadata that may have
been added to an inner group or specific endpoint.

C#

var outer = app.MapGroup("/outer");


var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>


{
app.Logger.LogInformation("/inner group filter");
return next(context);
});

outer.AddEndpointFilter((context, next) =>


{
app.Logger.LogInformation("/outer group filter");
return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>


{
app.Logger.LogInformation("MapGet filter");
return next(context);
});

In the above example, the outer filter will log the incoming request before the inner
filter even though it was added second. Because the filters were applied to different
groups, the order they were added relative to each other does not matter. The order
filters are added does matter if applied to the same group or specific endpoint.

A request to /outer/inner/ will log the following:

.NET CLI

/outer group filter


/inner group filter
MapGet filter

gRPC

JSON transcoding
gRPC JSON transcoding is an extension for ASP.NET Core that creates RESTful JSON APIs
for gRPC services. gRPC JSON transcoding allows:

Apps to call gRPC services with familiar HTTP concepts.


ASP.NET Core gRPC apps to support both gRPC and RESTful JSON APIs without
replicating functionality.
Experimental support for generating OpenAPI from transcoded RESTful APIs by
integrating with Swashbuckle.

For more information, see gRPC JSON transcoding in ASP.NET Core gRPC apps and Use
OpenAPI with gRPC JSON transcoding ASP.NET Core apps.

gRPC health checks in ASP.NET Core


The gRPC health checking protocol is a standard for reporting the health of gRPC
server apps. An app exposes health checks as a gRPC service. They are typically used
with an external monitoring service to check the status of an app.

gRPC ASP.NET Core has added built-in support for gRPC health checks with the
Grpc.AspNetCore.HealthChecks package. Results from .NET health checks are
reported to callers.

For more information, see gRPC health checks in ASP.NET Core.

Improved call credentials support


Call credentials are the recommended way to configure a gRPC client to send an auth
token to the server. gRPC clients support two new features to make call credentials
easier to use:

Support for call credentials with plaintext connections. Previously, a gRPC call only
sent call credentials if the connection was secured with TLS. A new setting on
GrpcChannelOptions , called UnsafeUseInsecureChannelCallCredentials , allows this

behavior to be customized. There are security implications to not securing a


connection with TLS.
A new method called AddCallCredentials is available with the gRPC client factory.
AddCallCredentials is a quick way to configure call credentials for a gRPC client

and integrates well with dependency injection (DI).

The following code configures the gRPC client factory to send Authorization metadata:

C#

builder.Services
.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
})
.AddCallCredentials((context, metadata) =>
{
if (!string.IsNullOrEmpty(_token))
{
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});

For more information, see Configure a bearer token with the gRPC client factory.

SignalR

Client results
The server now supports requesting a result from a client. This requires the server to use
ISingleClientProxy.InvokeAsync and the client to return a result from its .On handler.
Strongly-typed hubs can also return values from interface methods.

For more information, see Client results

Dependency injection for SignalR hub methods


SignalR hub methods now support injecting services through dependency injection (DI).

Hub constructors can accept services from DI as parameters, which can be stored in
properties on the class for use in a hub method. For more information, see Inject
services into a hub

Blazor

Handle location changing events and navigation state


In .NET 7, Blazor supports location changing events and maintaining navigation state.
This allows you to warn users about unsaved work or to perform related actions when
the user performs a page navigation.

For more information, see the following sections of the Routing and navigation article:

Navigation options
Handle/prevent location changes

Empty Blazor project templates


Blazor has two new project templates for starting from a blank slate. The new Blazor
Server App Empty and Blazor WebAssembly App Empty project templates are just like
their non-empty counterparts but without example code. These empty templates only
include a basic home page, and we've removed Bootstrap so that you can start with a
different CSS framework.

For more information, see the following articles:

Tooling for ASP.NET Core Blazor


ASP.NET Core Blazor project structure

Blazor custom elements


The Microsoft.AspNetCore.Components.CustomElements package enables building
standards based custom DOM elements using Blazor.

For more information, see ASP.NET Core Razor components.

Bind modifiers ( @bind:after , @bind:get , @bind:set )

) Important

The @bind:after / @bind:get / @bind:set features are receiving further updates at


this time. To take advantage of the latest updates, confirm that you've installed the
latest SDK .

Using an event callback parameter ( [Parameter] public EventCallback<string>


ValueChanged { get; set; } ) isn't supported. Instead, pass an Action-returning or

Task-returning method to @bind:set / @bind:after .

For more information, see the following resources:

Blazor @bind:after not working on .NET 7 RTM release (dotnet/aspnetcore


#44957)
BindGetSetAfter701 sample app ( javiercn/BindGetSetAfter701 GitHub
repository)

In .NET 7, you can run asynchronous logic after a binding event has completed using the
new @bind:after modifier. In the following example, the PerformSearch asynchronous
method runs automatically after any changes to the search text are detected:

razor

<input @bind="searchText" @bind:after="PerformSearch" />

@code {
private string searchText;

private async Task PerformSearch()


{
...
}
}

In .NET 7, it's also easier to set up binding for component parameters. Components can
support two-way data binding by defining a pair of parameters:
@bind:get : Specifies the value to bind.

@bind:set : Specifies a callback for when the value changes.

The @bind:get and @bind:set modifiers are always used together.

Examples:

razor

@* Elements *@

<input type="text" @bind="text" @bind:after="() => { }" />

<input type="text" @bind:get="text" @bind:set="(value) => { }" />

<input type="text" @bind="text" @bind:after="AfterAsync" />

<input type="text" @bind:get="text" @bind:set="SetAsync" />

<input type="text" @bind="text" @bind:after="() => { }" />

<input type="text" @bind:get="text" @bind:set="(value) => { }" />

<input type="text" @bind="text" @bind:after="AfterAsync" />

<input type="text" @bind:get="text" @bind:set="SetAsync" />

@* Components *@

<InputText @bind-Value="text" @bind-Value:after="() => { }" />

<InputText @bind-Value:get="text" @bind-Value:set="(value) => { }" />

<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />

<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

<InputText @bind-Value="text" @bind-Value:after="() => { }" />

<InputText @bind-Value:get="text" @bind-Value:set="(value) => { }" />

<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />

<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

@code {
private string text = "";

private void After(){}


private void Set() {}
private Task AfterAsync() { return Task.CompletedTask; }
private Task SetAsync(string value) { return Task.CompletedTask; }
}
For more information on the InputText component, see ASP.NET Core Blazor forms and
input components.

Hot Reload improvements


In .NET 7, Hot Reload support includes the following:

Components reset their parameters to their default values when a value is


removed.
Blazor WebAssembly:
Add new types.
Add nested classes.
Add static and instance methods to existing types.
Add static fields and methods to existing types.
Add static lambdas to existing methods.
Add lambdas that capture this to existing methods that already captured this
previously.

Dynamic authentication requests with MSAL in Blazor


WebAssembly
New in .NET 7, Blazor WebAssembly supports creating dynamic authentication requests
at runtime with custom parameters to handle advanced authentication scenarios.

For more information, see the following articles:

Secure ASP.NET Core Blazor WebAssembly


ASP.NET Core Blazor WebAssembly additional security scenarios

Blazor WebAssembly debugging improvements


Blazor WebAssembly debugging has the following improvements:

Support for the Just My Code setting to show or hide type members that aren't
from user code.
Support for inspecting multidimensional arrays.
Call Stack now shows the correct name for asynchronous methods.
Improved expression evaluation.
Correct handling of the new keyword on derived members.
Support for debugger-related attributes in System.Diagnostics .
System.Security.Cryptography support on WebAssembly

.NET 6 supported the SHA family of hashing algorithms when running on WebAssembly.
.NET 7 enables more cryptographic algorithms by taking advantage of SubtleCrypto ,
when possible, and falling back to a .NET implementation when SubtleCrypto can't be
used. The following algorithms are supported on WebAssembly in .NET 7:

SHA1
SHA256
SHA384
SHA512
HMACSHA1
HMACSHA256
HMACSHA384
HMACSHA512
AES-CBC
PBKDF2
HKDF

For more information, see Developers targeting browser-wasm can use Web Crypto APIs
(dotnet/runtime #40074) .

Inject services into custom validation attributes


You can now inject services into custom validation attributes. Blazor sets up the
ValidationContext so that it can be used as a service provider.

For more information, see ASP.NET Core Blazor forms and input components.

Input* components outside of an EditContext / EditForm

The built-in input components are now supported outside of a form in Razor
component markup.

For more information, see ASP.NET Core Blazor forms and input components.

Project template changes


When .NET 6 was released last year, the HTML markup of the _Host page
( Pages/_Host.chstml ) was split between the _Host page and a new _Layout page
( Pages/_Layout.chstml ) in the .NET 6 Blazor Server project template.
In .NET 7, the HTML markup has been recombined with the _Host page in project
templates.

Several additional changes were made to the Blazor project templates. It isn't feasible to
list every change to the templates in the documentation. To migrate an app to .NET 7 in
order to adopt all of the changes, see Migrate from ASP.NET Core 6.0 to 7.0.

Experimental QuickGrid component


The new QuickGrid component provides a convenient data grid component for most
common requirements and as a reference architecture and performance baseline for
anyone building Blazor data grid components.

For more information, see ASP.NET Core Blazor QuickGrid component.

Live demo: QuickGrid for Blazor sample app

Virtualization enhancements
Virtualization enhancements in .NET 7:

The Virtualize component supports using the document itself as the scroll root,
as an alternative to having some other element with overflow-y: scroll applied.
If the Virtualize component is placed inside an element that requires a specific
child tag name, SpacerElement allows you to obtain or set the virtualization spacer
tag name.

For more information, see the following sections of the Virtualization article:

Root-level virtualization
Control the spacer element tag name

MouseEventArgs updates

MovementX and MovementY have been added to MouseEventArgs .

For more information, see ASP.NET Core Blazor event handling.

New Blazor loading page


The Blazor WebAssembly project template has a new loading UI that shows the progress
of loading the app.
For more information, see ASP.NET Core Blazor startup.

Improved diagnostics for authentication in Blazor


WebAssembly
To help diagnose authentication issues in Blazor WebAssembly apps, detailed logging is
available.

For more information, see ASP.NET Core Blazor logging.

JavaScript interop on WebAssembly


JavaScript [JSImport] / [JSExport] interop API is a new low-level mechanism for using
.NET in Blazor WebAssembly and JavaScript-based apps. With this new JavaScript
interop capability, you can invoke .NET code from JavaScript using the .NET
WebAssembly runtime and call into JavaScript functionality from .NET without any
dependency on the Blazor UI component model.

For more information:

JavaScript JSImport/JSExport interop with ASP.NET Core Blazor WebAssembly:


Pertains only to Blazor WebAssembly apps.
Run .NET from JavaScript: Pertains only to JavaScript apps that don't depend on
the Blazor UI component model.

Conditional registration of the authentication state


provider
Prior to the release of .NET 7, AuthenticationStateProvider was registered in the service
container with AddScoped . This made it difficult to debug apps, as it forced a specific
order of service registrations when providing a custom implementation. Due to internal
framework changes over time, it's no longer necessary to register
AuthenticationStateProvider with AddScoped .

In developer code, make the following change to the authentication state provider
service registration:

diff

- builder.Services.AddScoped<AuthenticationStateProvider,
ExternalAuthStateProvider>();
+ builder.Services.TryAddScoped<AuthenticationStateProvider,
ExternalAuthStateProvider>();
In the preceding example, ExternalAuthStateProvider is the developer's service
implementation.

Improvements to the .NET WebAssembly build tools


New features in the wasm-tools workload for .NET 7 that help improve performance and
handle exceptions:

WebAssembly Single Instruction, Multiple Data (SIMD) support (only with AOT,
not supported by Apple Safari)
WebAssembly exception handling support

For more information, see Tooling for ASP.NET Core Blazor.

Blazor Hybrid

External URLs
An option has been added that permits opening external webpages in the browser.

For more information, see ASP.NET Core Blazor Hybrid routing and navigation.

Security
New guidance is available for Blazor Hybrid security scenarios. For more information,
see the following articles:

ASP.NET Core Blazor Hybrid authentication and authorization


ASP.NET Core Blazor Hybrid security considerations

Performance

Output caching middleware


Output caching is a new middleware that stores responses from a web app and serves
them from a cache rather than computing them every time. Output caching differs from
response caching in the following ways:

The caching behavior is configurable on the server.


Cache entries can be programmatically invalidated.
Resource locking mitigates the risk of cache stampede and thundering herd .
Cache revalidation means the server can return a 304 Not Modified HTTP status
code instead of a cached response body.
The cache storage medium is extensible.

For more information, see Overview of caching and Output caching middleware.

HTTP/3 improvements
This release:

Makes HTTP/3 fully supported by ASP.NET Core, it's no longer experimental.


Improves Kestrel's support for HTTP/3. The two main areas of improvement are
feature parity with HTTP/1.1 and HTTP/2, and performance.
Provides full support for UseHttps(ListenOptions, X509Certificate2) with HTTP/3.
Kestrel offers advanced options for configuring connection certificates, such as
hooking into Server Name Indication (SNI) .
Adds support for HTTP/3 on HTTP.sys and IIS.

The following example shows how to use an SNI callback to resolve TLS options:

C#

using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);


builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
listenOptions.UseHttps(new TlsHandshakeCallbackOptions
{
OnConnection = context =>
{
var options = new SslServerAuthenticationOptions
{
ServerCertificate =

MyResolveCertForHost(context.ClientHelloInfo.ServerName)
};
return new ValueTask<SslServerAuthenticationOptions>
(options);
},
});
});
});

Significant work was done in .NET 7 to reduce HTTP/3 allocations. You can see some of
those improvements in the following GitHub PR's:

HTTP/3: Avoid per-request cancellation token allocations


HTTP/3: Avoid ConnectionAbortedException allocations
HTTP/3: ValueTask pooling

HTTP/2 Performance improvements


.NET 7 introduces a significant re-architecture of how Kestrel processes HTTP/2 requests.
ASP.NET Core apps with busy HTTP/2 connections will experience reduced CPU usage
and higher throughput.

Previously, the HTTP/2 multiplexing implementation relied on a lock controlling which


request can write to the underlying TCP connection. A thread-safe queue replaces the
write lock. Now, rather than fighting over which thread gets to use the write lock,
requests now queue up and a dedicated consumer processes them. Previously wasted
CPU resources are available to the rest of the app.

One place where these improvements can be noticed is in gRPC, a popular RPC
framework that uses HTTP/2. Kestrel + gRPC benchmarks show a dramatic
improvement:
Changes were made in the HTTP/2 frame writing code that improves performance when
there are multiple streams trying to write data on a single HTTP/2 connection. We now
dispatch TLS work to the thread pool and more quickly release a write lock that other
streams can acquire to write their data. The reduction in wait times can yield significant
performance improvements in cases where there is contention for this write lock. A
gRPC benchmark with 70 streams on a single connection (with TLS) showed a ~15%
improvement in requests per second (RPS) with this change.

Http/2 WebSockets support


.NET 7 introduces Websockets over HTTP/2 support for Kestrel, the SignalR JavaScript
client, and SignalR with Blazor WebAssembly.

Using WebSockets over HTTP/2 takes advantage of new features such as:

Header compression.
Multiplexing, which reduces the time and resources needed when making multiple
requests to the server.

These supported features are available in Kestrel on all HTTP/2 enabled platforms. The
version negotiation is automatic in browsers and Kestrel, so no new APIs are needed.

For more information, see Http/2 WebSockets support.


Kestrel performance improvements on high core
machines
Kestrel uses ConcurrentQueue<T> for many purposes. One purpose is scheduling I/O
operations in Kestrel's default Socket transport. Partitioning the ConcurrentQueue based
on the associated socket reduces contention and increases throughput on machines
with many CPU cores.

Profiling on high core machines on .NET 6 showed significant contention in one of


Kestrel's other ConcurrentQueue instances, the PinnedMemoryPool that Kestrel uses to
cache byte buffers.

In .NET 7, Kestrel's memory pool is partitioned the same way as its I/O queue, which
leads to much lower contention and higher throughput on high core machines. On the
80 core ARM64 VMs, we're seeing over 500% improvement in responses per second
(RPS) in the TechEmpower plaintext benchmark. On 48 Core AMD VMs, the
improvement is nearly 100% in our HTTPS JSON benchmark.

ServerReady event to measure startup time

Apps using EventSource can measure the startup time to understand and optimize
startup performance. The new ServerReady event in Microsoft.AspNetCore.Hosting
represents the point where the server is ready to respond to requests.

Server

New ServerReady event for measuring startup time


The ServerReady event has been added to measure startup time of ASP.NET Core
apps.

IIS

Shadow copying in IIS


Shadow copying app assemblies to the ASP.NET Core Module (ANCM) for IIS can
provide a better end user experience than stopping the app by deploying an app offline
file.

For more information, see Shadow copying in IIS.


Miscellaneous

Kestrel full certificate chain improvements


HttpsConnectionAdapterOptions has a new ServerCertificateChain property of type
X509Certificate2Collection, which makes it easier to validate certificate chains by
allowing a full chain including intermediate certificates to be specified. See
dotnet/aspnetcore#21513 for more details.

dotnet watch

Improved console output for dotnet watch


The console output from dotnet watch has been improved to better align with the
logging of ASP.NET Core and to stand out with 😮emojis😍.

Here's an example of what the new output looks like:

For more information, see this GitHub pull request .

Configure dotnet watch to always restart for rude edits


Rude edits are edits that can't be hot reloaded. To configure dotnet watch to always
restart without a prompt for rude edits, set the DOTNET_WATCH_RESTART_ON_RUDE_EDIT
environment variable to true .

Developer exception page dark mode


Dark mode support has been added to the developer exception page, thanks to a
contribution by Patrick Westerhoff . To test dark mode in a browser, from the
developer tools page, set the mode to dark. For example, in Firefox:
In Chrome:
Project template option to use Program.Main method
instead of top-level statements
The .NET 7 templates include an option to not use top-level statements and generate a
namespace and a Main method declared on a Program class.

Using the .NET CLI, use the --use-program-main option:

.NET CLI

dotnet new web --use-program-main

With Visual Studio, select the new Do not use top-level statements checkbox during
project creation:

Updated Angular and React templates


The Angular project template has been updated to Angular 14. The React project
template has been updated to React 18.2.

Manage JSON Web Tokens in development with dotnet


user-jwts
The new dotnet user-jwts command line tool can create and manage app specific local
JSON Web Tokens (JWTs). For more information, see Manage JSON Web Tokens in
development with dotnet user-jwts.

Support for additional request headers in W3CLogger


You can now specify additional request headers to log when using the W3C logger by
calling AdditionalRequestHeaders() on W3CLoggerOptions:

C#

services.AddW3CLogging(logging =>
{
logging.AdditionalRequestHeaders.Add("x-forwarded-for");
logging.AdditionalRequestHeaders.Add("x-client-ssl-protocol");
});

For more information, see W3CLogger options.

Request decompression
The new Request decompression middleware:

Enables API endpoints to accept requests with compressed content.


Uses the Content-Encoding HTTP header to automatically identify and
decompress requests which contain compressed content.
Eliminates the need to write code to handle compressed requests.

For more information, see Request decompression middleware.


What's new in ASP.NET Core 6.0
Article • 10/29/2022

This article highlights the most significant changes in ASP.NET Core 6.0 with links to
relevant documentation.

ASP.NET Core MVC and Razor improvements

Minimal APIs
Minimal APIs are architected to create HTTP APIs with minimal dependencies. They are
ideal for microservices and apps that want to include only the minimum files, features,
and dependencies in ASP.NET Core. For more information, see:

Tutorial: Create a minimal API with ASP.NET Core


Differences between minimal APIs and APIs with controllers
Minimal APIs quick reference
Code samples migrated to the new minimal hosting model in 6.0

SignalR

Long running activity tag for SignalR connections


SignalR uses the new Microsoft.AspNetCore.Http.Features.IHttpActivityFeature.Activity
to add an http.long_running tag to the request activity. IHttpActivityFeature.Activity
is used by APM services like Azure Monitor Application Insights to filter SignalR
requests from creating long running request alerts.

SignalR performance improvements


Allocate HubCallerClients once per connection instead of every hub method call.
Avoid closure allocation in SignalR DefaultHubDispatcher.Invoke . State is passed to
a local static function via parameters to avoid a closure allocation. For more
information, see this GitHub pull request .
Allocate a single StreamItemMessage per stream instead of per stream item in
server-to-client streaming. For more information, see this GitHub pull request .
Razor compiler

Razor compiler updated to use source generators


The Razor compiler is now based on C# source generators. Source generators run
during compilation and inspect what is being compiled to produce additional files that
are compiled along with the rest of the project. Using source generators simplifies the
Razor compiler and significantly speeds up build times.

Razor compiler no longer produces a separate Views


assembly
The Razor compiler previously utilized a two-step compilation process that produced a
separate Views assembly that contained the generated views and pages ( .cshtml files)
defined in the app. The generated types were public and under the AspNetCore
namespace.

The updated Razor compiler builds the views and pages types into the main project
assembly. These types are now generated by default as internal sealed in the
AspNetCoreGeneratedDocument namespace. This change improves build performance,

enables single file deployment, and enables these types to participate in Hot Reload.

For more information about this change, see the related announcement issue on
GitHub.

ASP.NET Core performance and API


improvements
Many changes were made to reduce allocations and improve performance across the
stack:

Non-allocating app.Use extension method. The new overload of app.Use requires


passing the context to next which saves two internal per-request allocations that
are required when using the other overload.
Reduced memory allocations when accessing HttpRequest.Cookies. For more
information, see this GitHub issue .
Use LoggerMessage.Define for the windows only HTTP.sys web server. The ILogger
extension methods calls have been replaced with calls to LoggerMessage.Define .
Reduce the per connection overhead in SocketConnection by ~30%. For more
information, see this GitHub pull request .
Reduce allocations by removing logging delegates in generic types. For more
information, see this GitHub pull request .
Faster GET access (about 50%) to commonly-used features such as
IHttpRequestFeature, IHttpResponseFeature, IHttpResponseBodyFeature,
IRouteValuesFeature, and IEndpointFeature. For more information, see this GitHub
pull request .
Use single instance strings for known header names, even if they aren't in the
preserved header block. Using single instance string helps prevent multiple
duplicates of the same string in long lived connections, for example, in
Microsoft.AspNetCore.WebSockets. For more information, see this GitHub issue .
Reuse HttpProtocol CancellationTokenSource in Kestrel. Use the new
CancellationTokenSource.TryReset method on CancellationTokenSource to reuse
tokens if they haven’t been canceled. For more information, see this GitHub
issue and this video .
Implement and use an AdaptiveCapacityDictionary in Microsoft.AspNetCore.Http
RequestCookieCollection for more efficient access to dictionaries. For more
information, see this GitHub pull request .

Reduced memory footprint for idle TLS connections


For long running TLS connections where data is only occasionally sent back and forth,
we’ve significantly reduced the memory footprint of ASP.NET Core apps in .NET 6. This
should help improve the scalability of scenarios such as WebSocket servers. This was
possible due to numerous improvements in System.IO.Pipelines, SslStream, and Kestrel.
The following sections detail some of the improvements that have contributed to the
reduced memory footprint:

Reduce the size of System.IO.Pipelines.Pipe


For every connection that is established, two pipes are allocated in Kestrel:

The transport layer to the app for the request.


The application layer to the transport for the response.

By shrinking the size of System.IO.Pipelines.Pipe from 368 bytes to 264 bytes (about a
28.2% reduction), 208 bytes per connection are saved (104 bytes per Pipe).

Pool SocketSender
SocketSender objects (that subclass SocketAsyncEventArgs) are around 350 bytes at

runtime. Instead of allocating a new SocketSender object per connection, they can be
pooled. SocketSender objects can be pooled because sends are usually very fast. Pooling
reduces the per connection overhead. Instead of allocating 350 bytes per connection,
only pay 350 bytes per IOQueue are allocated. Allocation is done per queue to avoid
contention. Our WebSocket server with 5000 idle connections went from allocating
~1.75 MB (350 bytes * 5000) to allocating ~2.8 kb (350 bytes * 8) for SocketSender
objects.

Zero bytes reads with SslStream


Bufferless reads are a technique employed in ASP.NET Core to avoid renting memory
from the memory pool if there’s no data available on the socket. Prior to this change,
our WebSocket server with 5000 idle connections required ~200 MB without TLS
compared to ~800 MB with TLS. Some of these allocations (4k per connection) were
from Kestrel having to hold on to an ArrayPool<T> buffer while waiting for the reads on
SslStream to complete. Given that these connections were idle, none of reads completed
and returned their buffers to the ArrayPool , forcing the ArrayPool to allocate more
memory. The remaining allocations were in SslStream itself: 4k buffer for TLS
handshakes and 32k buffer for normal reads. In .NET 6, when the user performs a zero
byte read on SslStream and it has no data available, SslStream internally performs a
zero-byte read on the underlying wrapped stream. In the best case (idle connection),
these changes result in a savings of 40 Kb per connection while still allowing the
consumer (Kestrel) to be notified when data is available without holding on to any
unused buffers.

Zero byte reads with PipeReader

With bufferless reads supported on SslStream , an option was added to perform zero
byte reads to StreamPipeReader , the internal type that adapts a Stream into a
PipeReader . In Kestrel, a StreamPipeReader is used to adapt the underlying SslStream

into a PipeReader . Therefore it was necessary to expose these zero byte read semantics
on the PipeReader .

A PipeReader can now be created that supports zero bytes reads over any underlying
Stream that supports zero byte read semantics (e.g,. SslStream , NetworkStream, etc)

using the following API:

.NET CLI

var reader = PipeReader.Create(stream, new


StreamPipeReaderOptions(useZeroByteReads: true));
Remove slabs from the SlabMemoryPool

To reduce fragmentation of the heap, Kestrel employed a technique where it allocated


slabs of memory of 128 KB as part of its memory pool. The slabs were then further
divided into 4 KB blocks that were used by Kestrel internally. The slabs had to be larger
than 85 KB to force allocation on the large object heap to try and prevent the GC from
relocating this array. However, with the introduction of the new GC generation, Pinned
Object Heap (POH), it no longer makes sense to allocate blocks on slab. Kestrel now
directly allocates blocks on the POH, reducing the complexity involved in managing the
memory pool. This change should make easier to perform future improvements such as
making it easier to shrink the memory pool used by Kestrel.

IAsyncDisposable supported
IAsyncDisposable is now available for controllers, Razor Pages, and View Components.
Asynchronous versions have been added to the relevant interfaces in factories and
activators:

The new methods offer a default interface implementation that delegates to the
synchronous version and calls Dispose.
The implementations override the default implementation and handle disposing
IAsyncDisposable implementations.

The implementations favor IAsyncDisposable over IDisposable when both


interfaces are implemented.
Extenders must override the new methods included to support IAsyncDisposable
instances.

IAsyncDisposable is beneficial when working with:

Asynchronous enumerators, for example, in asynchronous streams.


Unmanaged resources that have resource-intensive I/O operations to release.

When implementing this interface, use the DisposeAsync method to release resources.

Consider a controller that creates and uses a Utf8JsonWriter. Utf8JsonWriter is an


IAsyncDisposable resource:

C#

public class HomeController : Controller, IAsyncDisposable


{
private Utf8JsonWriter? _jsonWriter;
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
_jsonWriter = new Utf8JsonWriter(new MemoryStream());
}

IAsyncDisposable must implement DisposeAsync :

C#

public async ValueTask DisposeAsync()


{
if (_jsonWriter is not null)
{
await _jsonWriter.DisposeAsync();
}

_jsonWriter = null;
}

Vcpkg port for SignalR C++ client


Vcpkg is a cross-platform command-line package manager for C and C++ libraries.
We’ve recently added a port to vcpkg to add CMake native support for the SignalR C++
client. vcpkg also works with MSBuild.

The SignalR client can be added to a CMake project with the following snippet when the
vcpkg is included in the toolchain file:

.NET CLI

find_package(microsoft-signalr CONFIG REQUIRED)


link_libraries(microsoft-signalr::microsoft-signalr)

With the preceding snippet, the SignalR C++ client is ready to use #include and used
in a project without any additional configuration. For a complete example of a C++
application that utilizes the SignalR C++ client, see the halter73/SignalR-Client-Cpp-
Sample repository.

Blazor

Project template changes


Several project template changes were made for Blazor apps, including the use of the
Pages/_Layout.cshtml file for layout content that appeared in the _Host.cshtml file for
earlier Blazor Server apps. Study the changes by creating an app from a 6.0 project
template or accessing the ASP.NET Core reference source for the project templates:

Blazor Server
Blazor WebAssembly

Blazor WebAssembly native dependencies support


Blazor WebAssembly apps can use native dependencies built to run on WebAssembly.
For more information, see ASP.NET Core Blazor WebAssembly native dependencies.

WebAssembly Ahead-of-time (AOT) compilation and


runtime relinking
Blazor WebAssembly supports ahead-of-time (AOT) compilation, where you can compile
your .NET code directly into WebAssembly. AOT compilation results in runtime
performance improvements at the expense of a larger app size. Relinking the .NET
WebAssembly runtime trims unused runtime code and thus improves download speed.
For more information, see Ahead-of-time (AOT) compilation and Runtime relinking.

Persist prerendered state


Blazor supports persisting state in a prerendered page so that the state doesn't need to
be recreated when the app is fully loaded. For more information, see Prerender and
integrate ASP.NET Core Razor components.

Error boundaries
Error boundaries provide a convenient approach for handling exceptions on the UI level.
For more information, see Handle errors in ASP.NET Core Blazor apps.

SVG support
The <foreignObject> element element is supported to display arbitrary HTML within
an SVG. For more information, see ASP.NET Core Razor components.

Blazor Server support for byte array transfer in JS Interop


Blazor supports optimized byte array JS interop that avoids encoding and decoding byte
arrays into Base64. For more information, see the following resources:

Call JavaScript functions from .NET methods in ASP.NET Core Blazor


Call .NET methods from JavaScript functions in ASP.NET Core Blazor

Query string enhancements


Support for working with query strings is improved. For more information, see ASP.NET
Core Blazor routing and navigation.

Binding to select multiple


Binding supports multiple option selection with <input> elements. For more
information, see the following resources:

ASP.NET Core Blazor data binding


ASP.NET Core Blazor forms and input components

Head ( <head> ) content control


Razor components can modify the HTML <head> element content of a page, including
setting the page's title ( <title> element) and modifying metadata ( <meta> elements).
For more information, see Control <head> content in ASP.NET Core Blazor apps.

Generate Angular and React components


Generate framework-specific JavaScript components from Razor components for web
frameworks, such as Angular or React. For more information, see ASP.NET Core Razor
components.

Render components from JavaScript


Render Razor components dynamically from JavaScript for existing JavaScript apps. For
more information, see ASP.NET Core Razor components.

Custom elements
Experimental support is available for building custom elements, which use standard
HTML interfaces. For more information, see ASP.NET Core Razor components.
Infer component generic types from ancestor
components
An ancestor component can cascade a type parameter by name to descendants using
the new [CascadingTypeParameter] attribute. For more information, see ASP.NET Core
Razor components.

Dynamically rendered components


Use the new built-in DynamicComponent component to render components by type. For
more information, see Dynamically-rendered ASP.NET Core Razor components.

Improved Blazor accessibility


Use the new FocusOnNavigate component to set the UI focus to an element based on a
CSS selector after navigating from one page to another. For more information, see
ASP.NET Core Blazor routing and navigation.

Custom event argument support


Blazor supports custom event arguments, which enable you to pass arbitrary data to
.NET event handlers with custom events. For more information, see ASP.NET Core Blazor
event handling.

Required parameters
Apply the new [EditorRequired] attribute to specify a required component parameter.
For more information, see ASP.NET Core Razor components.

Collocation of JavaScript files with pages, views, and


components
Collocate JavaScript files for pages, views, and Razor components as a convenient way
to organize scripts in an app. For more information, see ASP.NET Core Blazor JavaScript
interoperability (JS interop).

JavaScript initializers
JavaScript initializers execute logic before and after a Blazor app loads. For more
information, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

Streaming JavaScript interop


Blazor now supports streaming data directly between .NET and JavaScript. For more
information, see the following resources:

Stream from .NET to JavaScript


Stream from JavaScript to .NET

Generic type constraints


Generic type parameters are now supported. For more information, see ASP.NET Core
Razor components.

WebAssembly deployment layout


Use a deployment layout to enable Blazor WebAssembly app downloads in restricted
security environments. For more information, see Deployment layout for ASP.NET Core
Blazor WebAssembly apps.

New Blazor articles


In addition to the Blazor features described in the preceding sections, new Blazor
articles are available on the following subjects:

ASP.NET Core Blazor file downloads: Learn how to download a file using native
byte[] streaming interop to ensure efficient transfer to the client.
Work with images in ASP.NET Core Blazor: Discover how to work with images in
Blazor apps, including how to stream image data and preview an image.

Build Blazor Hybrid apps with .NET MAUI, WPF,


and Windows Forms
Use Blazor Hybrid to blend desktop and mobile native client frameworks with .NET and
Blazor:

.NET Multi-platform App UI (.NET MAUI) is a cross-platform framework for creating


native mobile and desktop apps with C# and XAML.
Blazor Hybrid apps can be built with Windows Presentation Foundation (WPF) and
Windows Forms frameworks.

) Important

Blazor Hybrid is in preview and shouldn't be used in production apps until final
release.

For more information, see the following resources:

Preview ASP.NET Core Blazor Hybrid documentation


What is .NET MAUI?
Microsoft .NET Blog (category: ".NET MAUI")

Kestrel
HTTP/3 is currently in draft and therefore subject to change. HTTP/3 support in
ASP.NET Core is not released, it's a preview feature included in .NET 6.

Kestrel now supports HTTP/3. For more information, see Use HTTP/3 with the ASP.NET
Core Kestrel web server and the blog entry HTTP/3 support in .NET 6 .

New Kestrel logging categories for selected logging


Prior to this change, enabling verbose logging for Kestrel was prohibitively expensive as
all of Kestrel shared the Microsoft.AspNetCore.Server.Kestrel logging category name.
Microsoft.AspNetCore.Server.Kestrel is still available, but the following new

subcategories allow for more control of logging:

Microsoft.AspNetCore.Server.Kestrel (current category): ApplicationError ,

ConnectionHeadResponseBodyWrite , ApplicationNeverCompleted , RequestBodyStart ,

RequestBodyDone , RequestBodyNotEntirelyRead , RequestBodyDrainTimedOut ,


ResponseMinimumDataRateNotSatisfied , InvalidResponseHeaderRemoved ,

HeartbeatSlow .
Microsoft.AspNetCore.Server.Kestrel.BadRequests : ConnectionBadRequest ,

RequestProcessingError , RequestBodyMinimumDataRateNotSatisfied .

Microsoft.AspNetCore.Server.Kestrel.Connections : ConnectionAccepted ,
ConnectionStart , ConnectionStop , ConnectionPause , ConnectionResume ,

ConnectionKeepAlive , ConnectionRejected , ConnectionDisconnect ,


NotAllConnectionsClosedGracefully , NotAllConnectionsAborted ,

ApplicationAbortedConnection .
Microsoft.AspNetCore.Server.Kestrel.Http2 : Http2ConnectionError ,

Http2ConnectionClosing , Http2ConnectionClosed , Http2StreamError ,


Http2StreamResetAbort , HPackDecodingError , HPackEncodingError ,

Http2FrameReceived , Http2FrameSending , Http2MaxConcurrentStreamsReached .

Microsoft.AspNetCore.Server.Kestrel.Http3 : Http3ConnectionError ,
Http3ConnectionClosing , Http3ConnectionClosed , Http3StreamAbort ,

Http3FrameReceived , Http3FrameSending .

Existing rules continue to work, but you can now be more selective on which rules you
enable. For example, the observability overhead of enabling Debug logging for just bad
requests is greatly reduced and can be enabled with the following configuration:

XML

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Kestrel.BadRequests": "Debug"
}
}

Log filtering applies rules with the longest matching category prefix. For more
information, see How filtering rules are applied

Emit KestrelServerOptions via EventSource event


The KestrelEventSource emits a new event containing the JSON-serialized
KestrelServerOptions when enabled with verbosity EventLevel.LogAlways . This event
makes it easier to reason about the server behavior when analyzing collected traces. The
following JSON is an example of the event payload:

JSON

{
"AllowSynchronousIO": false,
"AddServerHeader": true,
"AllowAlternateSchemes": false,
"AllowResponseHeaderCompression": true,
"EnableAltSvc": false,
"IsDevCertLoaded": true,
"RequestHeaderEncodingSelector": "default",
"ResponseHeaderEncodingSelector": "default",
"Limits": {
"KeepAliveTimeout": "00:02:10",
"MaxConcurrentConnections": null,
"MaxConcurrentUpgradedConnections": null,
"MaxRequestBodySize": 30000000,
"MaxRequestBufferSize": 1048576,
"MaxRequestHeaderCount": 100,
"MaxRequestHeadersTotalSize": 32768,
"MaxRequestLineSize": 8192,
"MaxResponseBufferSize": 65536,
"MinRequestBodyDataRate": "Bytes per second: 240, Grace Period:
00:00:05",
"MinResponseDataRate": "Bytes per second: 240, Grace Period: 00:00:05",
"RequestHeadersTimeout": "00:00:30",
"Http2": {
"MaxStreamsPerConnection": 100,
"HeaderTableSize": 4096,
"MaxFrameSize": 16384,
"MaxRequestHeaderFieldSize": 16384,
"InitialConnectionWindowSize": 131072,
"InitialStreamWindowSize": 98304,
"KeepAlivePingDelay": "10675199.02:48:05.4775807",
"KeepAlivePingTimeout": "00:00:20"
},
"Http3": {
"HeaderTableSize": 0,
"MaxRequestHeaderFieldSize": 16384
}
},
"ListenOptions": [
{
"Address": "https://127.0.0.1:7030",
"IsTls": true,
"Protocols": "Http1AndHttp2"
},
{
"Address": "https://[::1]:7030",
"IsTls": true,
"Protocols": "Http1AndHttp2"
},
{
"Address": "http://127.0.0.1:5030",
"IsTls": false,
"Protocols": "Http1AndHttp2"
},
{
"Address": "http://[::1]:5030",
"IsTls": false,
"Protocols": "Http1AndHttp2"
}
]
}
New DiagnosticSource event for rejected HTTP requests
Kestrel now emits a new DiagnosticSource event for HTTP requests rejected at the
server layer. Prior to this change, there was no way to observe these rejected requests.
The new DiagnosticSource event Microsoft.AspNetCore.Server.Kestrel.BadRequest
contains a IBadRequestExceptionFeature that can be used to introspect the reason for
rejecting the request.

C#

using Microsoft.AspNetCore.Http.Features;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();
var diagnosticSource = app.Services.GetRequiredService<DiagnosticListener>
();
using var badRequestListener = new BadRequestEventListener(diagnosticSource,
(badRequestExceptionFeature) =>
{
app.Logger.LogError(badRequestExceptionFeature.Error, "Bad request
received");
});
app.MapGet("/", () => "Hello world");

app.Run();

class BadRequestEventListener : IObserver<KeyValuePair<string, object>>,


IDisposable
{
private readonly IDisposable _subscription;
private readonly Action<IBadRequestExceptionFeature> _callback;

public BadRequestEventListener(DiagnosticListener diagnosticListener,


Action<IBadRequestExceptionFeature>
callback)
{
_subscription = diagnosticListener.Subscribe(this!, IsEnabled);
_callback = callback;
}
private static readonly Predicate<string> IsEnabled = (provider) =>
provider switch
{
"Microsoft.AspNetCore.Server.Kestrel.BadRequest" => true,
_ => false
};
public void OnNext(KeyValuePair<string, object> pair)
{
if (pair.Value is IFeatureCollection featureCollection)
{
var badRequestFeature =
featureCollection.Get<IBadRequestExceptionFeature>();
if (badRequestFeature is not null)
{
_callback(badRequestFeature);
}
}
}
public void OnError(Exception error) { }
public void OnCompleted() { }
public virtual void Dispose() => _subscription.Dispose();
}

For more information, see Logging and diagnostics in Kestrel.

Create a ConnectionContext from an Accept Socket


The new SocketConnectionContextFactory makes it possible to create a
ConnectionContext from an accepted socket. This makes it possible to build a custom
socket-based IConnectionListenerFactory without losing out on all the performance
work and pooling happening in SocketConnection .

See this example of a custom IConnectionListenerFactory which shows how to use this
SocketConnectionContextFactory .

Kestrel is the default launch profile for Visual Studio


The default launch profile for all new dotnet web projects is Kestrel. Starting Kestrel is
significantly faster and results in a more responsive experience while developing apps.

IIS Express is still available as a launch profile for scenarios such as Windows
Authentication or port sharing.

Localhost ports for Kestrel are random


See Template generated ports for Kestrel in this document for more information.

Authentication and authorization

Authentication servers
.NET 3 to .NET 5 used IdentityServer4 as part of our template to support the issuing of
JWT tokens for SPA and Blazor applications. The templates now use the Duende Identity
Server .

If you are extending the identity models and are updating existing projects you need to
update the namespaces in your code from IdentityServer4.IdentityServer to
Duende.IdentityServer and follow their migration instructions .

The license model for Duende Identity Server has changed to a reciprocal license, which
may require license fees when it's used commercially in production. See the Duende
license page for more details.

Delayed client certificate negotiation


Developers can now opt-in to using delayed client certificate negotiation by specifying
ClientCertificateMode.DelayCertificate on the HttpsConnectionAdapterOptions. This
only works with HTTP/1.1 connections because HTTP/2 forbids delayed certificate
renegotiation. The caller of this API must buffer the request body before requesting the
client certificate:

C#

using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.WebUtilities;

var builder = WebApplication.CreateBuilder(args);


builder.WebHost.UseKestrel(options =>
{
options.ConfigureHttpsDefaults(adapterOptions =>
{
adapterOptions.ClientCertificateMode =
ClientCertificateMode.DelayCertificate;
});
});

var app = builder.Build();


app.Use(async (context, next) =>
{
bool desiredState = GetDesiredState();
// Check if your desired criteria is met
if (desiredState)
{
// Buffer the request body
context.Request.EnableBuffering();
var body = context.Request.Body;
await body.DrainAsync(context.RequestAborted);
body.Position = 0;

// Request client certificate


var cert = await context.Connection.GetClientCertificateAsync();
// Disable buffering on future requests if the client doesn't
provide a cert
}
await next(context);
});

app.MapGet("/", () => "Hello World!");


app.Run();

OnCheckSlidingExpiration event for controlling cookie


renewal
Cookie authentication sliding expiration can now be customized or suppressed using
the new OnCheckSlidingExpiration. For example, this event can be used by a single-page
app that needs to periodically ping the server without affecting the authentication
session.

Miscellaneous

Hot Reload
Quickly make UI and code updates to running apps without losing app state for faster
and more productive developer experience using Hot Reload. For more information, see
.NET Hot Reload support for ASP.NET Core and Update on .NET Hot Reload progress
and Visual Studio 2022 Highlights .

Improved single-page app (SPA) templates


The ASP.NET Core project templates have been updated for Angular and React to use an
improved pattern for single-page apps that is more flexible and more closely aligns with
common patterns for modern front-end web development.

Previously, the ASP.NET Core template for Angular and React used specialized
middleware during development to launch the development server for the front-end
framework and then proxy requests from ASP.NET Core to the development server. The
logic for launching the front-end development server was specific to the command-line
interface for the corresponding front-end framework. Supporting additional front-end
frameworks using this pattern meant adding additional logic to ASP.NET Core.

The updated ASP.NET Core templates for Angular and React in .NET 6 flips this
arrangement around and take advantage of the built-in proxying support in the
development servers of most modern front-end frameworks. When the ASP.NET Core
app is launched, the front-end development server is launched just as before, but the
development server is configured to proxy requests to the backend ASP.NET Core
process. All of the front-end specific configuration to setup proxying is part of the app,
not ASP.NET Core. Setting up ASP.NET Core projects to work with other front-end
frameworks is now straight-forward: setup the front-end development server for the
chosen framework to proxy to the ASP.NET Core backend using the pattern established
in the Angular and React templates.

The startup code for the ASP.NET Core app no longer needs any single-page app-
specific logic. The logic for starting the front-end development server during
development is injecting into the app at runtime by the new
Microsoft.AspNetCore.SpaProxy package. Fallback routing is handled using endpoint
routing instead of SPA-specific middleware.

Templates that follow this pattern can still be run as a single project in Visual Studio or
using dotnet run from the command-line. When the app is published, the front-end
code in the ClientApp folder is built and collected as before into the web root of the
host ASP.NET Core app and served as static files. Scripts included in the template
configure the front-end development server to use HTTPS using the ASP.NET Core
development certificate.

Draft HTTP/3 support in .NET 6


HTTP/3 is currently in draft and therefore subject to change. HTTP/3 support in
ASP.NET Core is not released, it's a preview feature included in .NET 6.

See the blog entry HTTP/3 support in .NET 6 .

Nullable Reference Type Annotations


Portions of the ASP.NET Core 6.0 source code has had nullability annotations applied.

By utilizing the new Nullable feature in C# 8, ASP.NET Core can provide additional
compile-time safety in the handling of reference types. For example, protecting against
null reference exceptions. Projects that have opted in to using nullable annotations
may see new build-time warnings from ASP.NET Core APIs.

To enable nullable reference types, add the following property to project files:

XML
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>

For more information, see Nullable reference types.

Source Code Analysis


Several .NET compiler platform analyzers were added that inspect application code for
problems such as incorrect middleware configuration or order, routing conflicts, etc. For
more information, see Code analysis in ASP.NET Core apps.

Web app template improvements


The web app templates:

Use the new minimal hosting model.


Significantly reduces the number of files and lines of code required to create an
app. For example, the ASP.NET Core empty web app creates one C# file with four
lines of code and is a complete app.
Unifies Startup.cs and Program.cs into a single Program.cs file.
Uses top-level statements to minimize the code required for an app.
Uses global using directives to eliminate or minimize the number of using
statement lines required.

Template generated ports for Kestrel


Random ports are assigned during project creation for use by the Kestrel web server.
Random ports help minimize a port conflict when multiple projects are run on the same
machine.

When a project is created, a random HTTP port between 5000-5300 and a random
HTTPS port between 7000-7300 is specified in the generated
Properties/launchSettings.json file. The ports can be changed in the

Properties/launchSettings.json file. If no port is specified, Kestrel defaults to the HTTP


5000 and HTTPS 5001 ports. For more information, see Configure endpoints for the
ASP.NET Core Kestrel web server.

New logging defaults


The following changes were made to both appsettings.json and
appsettings.Development.json :

diff

- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
+ "Microsoft.AspNetCore": "Warning"

The change from "Microsoft": "Warning" to "Microsoft.AspNetCore": "Warning" results


in logging all informational messages from the Microsoft namespace except
Microsoft.AspNetCore . For example, Microsoft.EntityFrameworkCore is now logged at

the informational level.

Developer exception page Middleware added


automatically
In the development environment, the DeveloperExceptionPageMiddleware is added by
default. It's no longer necessary to add the following code to web UI apps:

C#

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

Support for Latin1 encoded request headers in


HttpSysServer
HttpSysServer now supports decoding request headers that are Latin1 encoded by

setting the UseLatin1RequestHeaders property on HttpSysOptions to true :

C#

var builder = WebApplication.CreateBuilder(args);


builder.WebHost.UseHttpSys(o => o.UseLatin1RequestHeaders = true);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
The ASP.NET Core Module logs include timestamps and
PID
The ASP.NET Core Module (ANCM) for IIS (ANCM) enhanced diagnostic logs include
timestamps and PID of the process emitting the logs. Logging timestamps and PID
makes it easier to diagnose issues with overlapping process restarts in IIS when multiple
IIS worker processes are running.

The resulting logs now resemble the sample output show below:

.NET CLI

[2021-07-28T19:23:44.076Z, PID: 11020] [aspnetcorev2.dll] Initializing logs


for 'C:\<path>\aspnetcorev2.dll'. Process Id: 11020. File Version:
16.0.21209.0. Description: IIS ASP.NET Core Module V2. Commit:
96475a2acdf50d7599ba8e96583fa73efbe27912.
[2021-07-28T19:23:44.079Z, PID: 11020] [aspnetcorev2.dll] Resolving hostfxr
parameters for application: '.\InProcessWebSite.exe' arguments: '' path:
'C:\Temp\e86ac4e9ced24bb6bacf1a9415e70753\'
[2021-07-28T19:23:44.080Z, PID: 11020] [aspnetcorev2.dll] Known dotnet.exe
location: ''

Configurable unconsumed incoming buffer size for IIS


The IIS server previously only buffered 64 KiB of unconsumed request bodies. The 64 KiB
buffering resulted in reads being constrained to that maximum size, which impacts the
performance with large incoming bodies such as uploads. In .NET 6 , the default buffer
size changes from 64 KiB to 1 MiB which should improve throughput for large uploads.
In our tests, a 700 MiB upload that used to take 9 seconds now only takes 2.5 seconds.

The downside of a larger buffer size is an increased per-request memory consumption


when the app isn’t quickly reading from the request body. So, in addition to changing
the default buffer size, the buffer size configurable, allowing apps to configure the
buffer size based on workload.

View Components Tag Helpers


Consider a view component with an optional parameter, as shown in the following code:

C#

class MyViewComponent
{
IViewComponentResult Invoke(bool showSomething = false) { ... }
}
With ASP.NET Core 6, the tag helper can be invoked without having to specify a value
for the showSomething parameter:

razor

<vc:my />

Angular template updated to Angular 12


The ASP.NET Core 6.0 template for Angular now uses Angular 12 .

The React template has been updated to React 17 .

Configurable buffer threshold before writing to disk in


Json.NET output formatter
Note: We recommend using the System.Text.Json output formatter except when the
Newtonsoft.Json serializer is required for compatibility reasons. The System.Text.Json
serializer is fully async and works efficiently for larger payloads.

The Newtonsoft.Json output formatter by default buffers responses up to 32 KiB in


memory before buffering to disk. This is to avoid performing synchronous IO, which can
result in other side-effects such as thread starvation and application deadlocks.
However, if the response is larger than 32 KiB, considerable disk I/O occurs. The memory
threshold is now configurable via the
MvcNewtonsoftJsonOptions.OutputFormatterMemoryBufferThreshold property before
buffering to disk:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages()
.AddNewtonsoftJson(options =>
{
options.OutputFormatterMemoryBufferThreshold = 48 * 1024;
});

var app = builder.Build();

For more information, see this GitHub pull request and the
NewtonsoftJsonOutputFormatterTest.cs file.
Faster get and set for HTTP headers
New APIs were added to expose all common headers available on
Microsoft.Net.Http.Headers.HeaderNames as properties on the IHeaderDictionary
resulting in an easier to use API. For example, the in-line middleware in the following
code gets and sets both request and response headers using the new APIs:

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Use(async (context, next) =>


{
var hostHeader = context.Request.Headers.Host;
app.Logger.LogInformation("Host header: {host}", hostHeader);
context.Response.Headers.XPoweredBy = "ASP.NET Core 6.0";
await next.Invoke(context);
var dateHeader = context.Response.Headers.Date;
app.Logger.LogInformation("Response date: {date}", dateHeader);
});

app.Run();

For implemented headers the get and set accessors are implemented by going directly
to the field and bypassing the lookup. For non-implemented headers, the accessors can
bypass the initial lookup against implemented headers and directly perform the
Dictionary<string, StringValues> lookup. Avoiding the lookup results in faster access
for both scenarios.

Async streaming
ASP.NET Core now supports asynchronous streaming from controller actions and
responses from the JSON formatter. Returning an IAsyncEnumerable from an action no
longer buffers the response content in memory before it gets sent. Not buffering helps
reduce memory usage when returning large datasets that can be asynchronously
enumerated.

Note that Entity Framework Core provides implementations of IAsyncEnumerable for


querying the database. The improved support for IAsyncEnumerable in ASP.NET Core in
.NET 6 can make using EF Core with ASP.NET Core more efficient. For example, the
following code no longer buffers the product data into memory before sending the
response:

C#

public IActionResult GetMovies()


{
return Ok(_context.Movie);
}

However, when using lazy loading in EF Core, this new behavior may result in errors due
to concurrent query execution while the data is being enumerated. Apps can revert back
to the previous behavior by buffering the data:

C#

public async Task<IActionResult> GetMovies2()


{
return Ok(await _context.Movie.ToListAsync());
}

See the related announcement for additional details about this change in behavior.

HTTP logging middleware


HTTP logging is a new built-in middleware that logs information about HTTP requests
and HTTP responses including the headers and entire body:

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();


app.UseHttpLogging();

app.MapGet("/", () => "Hello World!");

app.Run();

Navigating to / with the previous code logs information similar to the following output:

.NET CLI

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
Request:
Protocol: HTTP/2
Method: GET
Scheme: https
PathBase:
Path: /
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,
*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cache-Control: max-age=0
Connection: close
Cookie: [Redacted]
Host: localhost:44372
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Edg/95.0.1020.30
sec-ch-ua: [Redacted]
sec-ch-ua-mobile: [Redacted]
sec-ch-ua-platform: [Redacted]
upgrade-insecure-requests: [Redacted]
sec-fetch-site: [Redacted]
sec-fetch-mode: [Redacted]
sec-fetch-user: [Redacted]
sec-fetch-dest: [Redacted]
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
Response:
StatusCode: 200
Content-Type: text/plain; charset=utf-8

The preceding output was enabled with the following appsettings.Development.json


file:

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware":
"Information"
}
}
}

HTTP logging provides logs of:

HTTP Request information


Common properties
Headers
Body
HTTP Response information

To configure the HTTP logging middleware, specify HttpLoggingOptions:

C#

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddHttpLogging(logging =>
{
// Customize HTTP logging.
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestHeaders.Add("My-Request-Header");
logging.ResponseHeaders.Add("My-Response-Header");
logging.MediaTypeOptions.AddText("application/javascript");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;
});

var app = builder.Build();


app.UseHttpLogging();

app.MapGet("/", () => "Hello World!");

app.Run();

IConnectionSocketFeature
The IConnectionSocketFeature request feature provides access to the underlying accept
socket associated with the current request. It can be accessed via the FeatureCollection
on HttpContext .

For example, the following app sets the LingerState property on the accepted socket:

C#

var builder = WebApplication.CreateBuilder(args);


builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.ConfigureEndpointDefaults(listenOptions =>
listenOptions.Use((connection, next) =>
{
var socketFeature =
connection.Features.Get<IConnectionSocketFeature>();
socketFeature.Socket.LingerState = new LingerOption(true, seconds:
10);
return next();
}));
});
var app = builder.Build();
app.MapGet("/", (Func<string>)(() => "Hello world"));
await app.RunAsync();

Generic type constraints in Razor


When defining generic type parameters in Razor using the @typeparam directive, generic
type constraints can now be specified using the standard C# syntax:

Smaller SignalR, Blazor Server, and MessagePack scripts


The SignalR, MessagePack, and Blazor Server scripts are now significantly smaller,
enabling smaller downloads, less JavaScript parsing and compiling by the browser, and
faster start-up. The size reductions:

signalr.js : 70%
blazor.server.js : 45%

The smaller scripts are a result of a community contribution from Ben Adams . For
more information on the details of the size reduction, see Ben's GitHub pull request .

Enable Redis profiling sessions


A community contribution from Gabriel Lucaci enables Redis profiling session with
Microsoft.Extensions.Caching.StackExchangeRedis :

C#

using StackExchange.Redis.Profiling;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddStackExchangeRedisCache(options =>
{
options.ProfilingSession = () => new ProfilingSession();
});

For more information, see StackExchange.Redis Profiling .

Shadow copying in IIS


An experimental feature has been added to the ASP.NET Core Module (ANCM) for IIS to
add support for shadow copying application assemblies. Currently .NET locks application
binaries when running on Windows making it impossible to replace binaries when the
app is running. While our recommendation remains to use an app offline file, we
recognize there are certain scenarios (for example FTP deployments) where it isn’t
possible to do so.

In such scenarios, enable shadow copying by customizing the ASP.NET Core module
handler settings. In most cases, ASP.NET Core apps do not have a web.config checked
into source control that you can modify. In ASP.NET Core, web.config is ordinarily
generated by the SDK. The following sample web.config can be used to get started:

XML

<?xml version="1.0" encoding="utf-8"?>


<configuration>
<!-- To customize the asp.net core module uncomment and edit the following
section.
For more info see https://go.microsoft.com/fwlink/?linkid=838655 -->

<system.webServer>
<handlers>
<remove name="aspNetCore"/>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2"
resourceType="Unspecified"/>
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%"
stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout">
<handlerSettings>
<handlerSetting name="experimentalEnableShadowCopy" value="true" />
<handlerSetting name="shadowCopyDirectory"
value="../ShadowCopyDirectory/" />
<!-- Only enable handler logging if you encounter issues-->
<!--<handlerSetting name="debugFile" value=".\logs\aspnetcore-
debug.log" />-->
<!--<handlerSetting name="debugLevel" value="FILE,TRACE" />-->
</handlerSettings>
</aspNetCore>
</system.webServer>
</configuration>

Shadow copying in IIS is an experimental feature that is not guaranteed to be part of


ASP.NET Core. Please leave feedback on IIS Shadow copying in this GitHub issue .

Additional resources
Code samples migrated to the new minimal hosting model in 6.0
What's new in .NET 6
What's new in ASP.NET Core 5.0
Article • 09/21/2022

This article highlights the most significant changes in ASP.NET Core 5.0 with links to
relevant documentation.

ASP.NET Core MVC and Razor improvements

Model binding DateTime as UTC


Model binding now supports binding UTC time strings to DateTime . If the request
contains a UTC time string, model binding binds it to a UTC DateTime . For example, the
following time string is bound the UTC DateTime :
https://example.com/mycontroller/myaction?time=2019-06-14T02%3A30%3A04.0576719Z

Model binding and validation with C# 9 record types


C# 9 record types can be used with model binding in an MVC controller or a Razor Page.
Record types are a good way to model data being transmitted over the network.

For example, the following PersonController uses the Person record type with model
binding and form validation:

C#

public record Person([Required] string Name, [Range(0, 150)] int Age);

public class PersonController


{
public IActionResult Index() => View();

[HttpPost]
public IActionResult Index(Person person)
{
// ...
}
}

The Person/Index.cshtml file:

CSHTML
@model Person

Name: <input asp-for="Model.Name" />


<span asp-validation-for="Model.Name" />

Age: <input asp-for="Model.Age" />


<span asp-validation-for="Model.Age" />

Improvements to DynamicRouteValueTransformer
ASP.NET Core 3.1 introduced DynamicRouteValueTransformer as a way to use custom
endpoint to dynamically select an MVC controller action or a Razor page. ASP.NET Core
5.0 apps can pass state to a DynamicRouteValueTransformer and filter the set of
endpoints chosen.

Miscellaneous
The [Compare] attribute can be applied to properties on a Razor Page model.
Parameters and properties bound from the body are considered required by
default.

Web API

OpenAPI Specification on by default


OpenAPI Specification is an industry standard for describing HTTP APIs and
integrating them into complex business processes or with third parties. OpenAPI is
widely supported by all cloud providers and many API registries. Apps that emit
OpenAPI documents from web APIs have a variety of new opportunities in which those
APIs can be used. In partnership with the maintainers of the open-source project
Swashbuckle.AspNetCore , the ASP.NET Core API template contains a NuGet
dependency on Swashbuckle . Swashbuckle is a popular open-source NuGet package
that emits OpenAPI documents dynamically. Swashbuckle does this by introspecting
over the API controllers and generating the OpenAPI document at run-time, or at build
time using the Swashbuckle CLI.

In ASP.NET Core 5.0, the web API templates enable the OpenAPI support by default. To
disable OpenAPI:

From the command line:


.NET CLI

dotnet new webapi --no-openapi true

From Visual Studio: Uncheck Enable OpenAPI support.

All .csproj files created for web API projects contain the Swashbuckle.AspNetCore
NuGet package reference.

XML

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
</ItemGroup>

The template generated code contains code in Startup.ConfigureServices that activates


OpenAPI document generation:

C#

public void ConfigureServices(IServiceCollection services)


{

services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApp1", Version =
"v1" });
});
}

The Startup.Configure method adds the Swashbuckle middleware, which enables the:

Document generation process.


Swagger UI page by default in development mode.

The template generated code won't accidentally expose the API's description when
publishing to production.

C#

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
"WebApp1 v1"));
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

Azure API Management Import


When ASP.NET Core API projects enable OpenAPI, the Visual Studio 2019 version 16.8
and later publishing automatically offer an additional step in the publishing flow.
Developers who use Azure API Management have an opportunity to automatically
import the APIs into Azure API Management during the publish flow:

Better launch experience for web API projects


With OpenAPI enabled by default, the app launching experience (F5) for web API
developers significantly improves. With ASP.NET Core 5.0, the web API template comes
pre-configured to load up the Swagger UI page. The Swagger UI page provides both the
documentation added for the published API, and enables testing the APIs with a single
click.

Blazor

Performance improvements
For .NET 5, we made significant improvements to Blazor WebAssembly runtime
performance with a specific focus on complex UI rendering and JSON serialization. In
our performance tests, Blazor WebAssembly in .NET 5 is two to three times faster for
most scenarios. For more information, see ASP.NET Blog: ASP.NET Core updates in .NET
5 Release Candidate 1 .

CSS isolation
Blazor now supports defining CSS styles that are scoped to a given component.
Component-specific CSS styles make it easier to reason about the styles in an app and
to avoid unintentional side effects of global styles. For more information, see ASP.NET
Core Blazor CSS isolation.
New InputFile component
The InputFile component allows reading one or more files selected by a user for
upload. For more information, see ASP.NET Core Blazor file uploads.

New InputRadio and InputRadioGroup components


Blazor has built-in InputRadio and InputRadioGroup components that simplify data
binding to radio button groups with integrated validation. For more information, see
ASP.NET Core Blazor forms and input components.

Component virtualization
Improve the perceived performance of component rendering using the Blazor
framework's built-in virtualization support. For more information, see ASP.NET Core
Razor component virtualization.

ontoggle event support

Blazor events now support the ontoggle DOM event. For more information, see ASP.NET
Core Blazor event handling.

Set UI focus in Blazor apps


Use the FocusAsync convenience method on element references to set the UI focus to
that element. For more information, see ASP.NET Core Blazor event handling.

Custom validation CSS class attributes


Custom validation CSS class attributes are useful when integrating with CSS frameworks,
such as Bootstrap. For more information, see ASP.NET Core Blazor forms and input
components.

IAsyncDisposable support
Razor components now support the IAsyncDisposable interface for the asynchronous
release of allocated resources.

JavaScript isolation and object references


Blazor enables JavaScript isolation in standard JavaScript modules . For more
information, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

Form components support display name


The following built-in components support display names with the DisplayName
parameter:

InputDate

InputNumber
InputSelect

For more information, see ASP.NET Core Blazor forms and input components.

Catch-all route parameters


Catch-all route parameters, which capture paths across multiple folder boundaries, are
supported in components. For more information, see ASP.NET Core Blazor routing and
navigation.

Debugging improvements
Debugging Blazor WebAssembly apps is improved in ASP.NET Core 5.0. Additionally,
debugging is now supported on Visual Studio for Mac. For more information, see Debug
ASP.NET Core Blazor WebAssembly.

Microsoft Identity v2.0 and MSAL v2.0


Blazor security now uses Microsoft Identity v2.0 (Microsoft.Identity.Web and
Microsoft.Identity.Web.UI ) and MSAL v2.0. For more information, see the topics in the
Blazor Security and Identity node.

Protected Browser Storage for Blazor Server


Blazor Server apps can now use built-in support for storing app state in the browser that
has been protected from tampering using ASP.NET Core data protection. Data can be
stored in either local browser storage or session storage. For more information, see
ASP.NET Core Blazor state management.

Blazor WebAssembly prerendering


Component integration is improved across hosting models, and Blazor WebAssembly
apps can now prerender output on the server.

Trimming/linking improvements
Blazor WebAssembly performs Intermediate Language (IL) trimming/linking during a
build to trim unnecessary IL from the app's output assemblies. With the release of
ASP.NET Core 5.0, Blazor WebAssembly performs improved trimming with additional
configuration options. For more information, see Configure the Trimmer for ASP.NET
Core Blazor and Trimming options.

Browser compatibility analyzer


Blazor WebAssembly apps target the full .NET API surface area, but not all .NET APIs are
supported on WebAssembly due to browser sandbox constraints. Unsupported APIs
throw PlatformNotSupportedException when running on WebAssembly. A platform
compatibility analyzer warns the developer when the app uses APIs that aren't
supported by the app's target platforms. For more information, see Consume ASP.NET
Core Razor components from a Razor class library (RCL).

Lazy load assemblies


Blazor WebAssembly app startup performance can be improved by deferring the
loading of some application assemblies until they're required. For more information, see
Lazy load assemblies in ASP.NET Core Blazor WebAssembly.

Updated globalization support


Globalization support is available for Blazor WebAssembly based on International
Components for Unicode (ICU). For more information, see ASP.NET Core Blazor
globalization and localization.

gRPC
Many preformance improvements have been made in gRPC . For more information,
see gRPC performance improvements in .NET 5 .

For more gRPC information, see Overview for gRPC on .NET.


SignalR

SignalR Hub filters


SignalR Hub filters, called Hub pipelines in ASP.NET SignalR, is a feature that allows code
to run before and after Hub methods are called. Running code before and after Hub
methods are called is similar to how middleware has the ability to run code before and
after an HTTP request. Common uses include logging, error handling, and argument
validation.

For more information, see Use hub filters in ASP.NET Core SignalR.

SignalR parallel hub invocations


ASP.NET Core SignalR is now capable of handling parallel hub invocations. The default
behavior can be changed to allow clients to invoke more than one hub method at a
time:

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddSignalR(options =>
{
options.MaximumParallelInvocationsPerClient = 5;
});
}

Added Messagepack support in SignalR Java client


A new package, com.microsoft.signalr.messagepack , adds MessagePack support to
the SignalR Java client. To use the MessagePack hub protocol, add .withHubProtocol(new
MessagePackHubProtocol()) to the connection builder:

Java

HubConnection hubConnection = HubConnectionBuilder.create(


"http://localhost:53353/MyHub")
.withHubProtocol(new MessagePackHubProtocol())
.build();

Kestrel
Reloadable endpoints via configuration: Kestrel can detect changes to
configuration passed to KestrelServerOptions.Configure and unbind from existing
endpoints and bind to new endpoints without requiring an application restart
when the reloadOnChange parameter is true . By default when using
ConfigureWebHostDefaults or CreateDefaultBuilder, Kestrel binds to the
"Kestrel" configuration subsection with reloadOnChange enabled. Apps must pass
reloadOnChange: true when calling KestrelServerOptions.Configure manually to
get reloadable endpoints.

HTTP/2 response headers improvements. For more information, see Performance


improvements in the next section.

Support for additional endpoints types in the sockets transport: Adding to the new
API introduced in System.Net.Sockets, the sockets default transport in Kestrel
allows binding to both existing file handles and Unix domain sockets. Support for
binding to existing file handles enables using the existing Systemd integration
without requiring the libuv transport.

Custom header decoding in Kestrel: Apps can specify which Encoding to use to
interpret incoming headers based on the header name instead of defaulting to
UTF-8. Set the
Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions.RequestHeaderEncoding
Selector property to specify which encoding to use:

C#

public static IHostBuilder CreateHostBuilder(string[] args) =>


Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
options.RequestHeaderEncodingSelector = encoding =>
{
return encoding switch
{
"Host" => System.Text.Encoding.Latin1,
_ => System.Text.Encoding.UTF8,
};
};
});
webBuilder.UseStartup<Startup>();
});

Kestrel endpoint-specific options via configuration


Support has been added for configuring Kestrel’s endpoint-specific options via
configuration. The endpoint-specific configurations includes the:

HTTP protocols used


TLS protocols used
Certificate selected
Client certificate mode

Configuration allows specifying which certificate is selected based on the specified


server name. The server name is part of the Server Name Indication (SNI) extension to
the TLS protocol as indicated by the client. Kestrel's configuration also supports a
wildcard prefix in the host name.

The following example shows how to specify endpoint-specific using a configuration file:

JSON

{
"Kestrel": {
"Endpoints": {
"EndpointName": {
"Url": "https://*",
"Sni": {
"a.example.org": {
"Protocols": "Http1AndHttp2",
"SslProtocols": [ "Tls11", "Tls12"],
"Certificate": {
"Path": "testCert.pfx",
"Password": "testPassword"
},
"ClientCertificateMode" : "NoCertificate"
},
"*.example.org": {
"Certificate": {
"Path": "testCert2.pfx",
"Password": "testPassword"
}
},
"*": {
// At least one sub-property needs to exist per
// SNI section or it cannot be discovered via
// IConfiguration
"Protocols": "Http1",
}
}
}
}
}
}
Server Name Indication (SNI) is a TLS extension to include a virtual domain as a part of
SSL negotiation. What this effectively means is that the virtual domain name, or a
hostname, can be used to identify the network end point.

Performance improvements

HTTP/2
Significant reductions in allocations in the HTTP/2 code path.

Support for HPack dynamic compression of HTTP/2 response headers in Kestrel.


For more information, see Header table size and HPACK: the silent killer (feature) of
HTTP/2 .

Sending HTTP/2 PING frames: HTTP/2 has a mechanism for sending PING frames
to ensure an idle connection is still functional. Ensuring a viable connection is
especially useful when working with long-lived streams that are often idle but only
intermittently see activity, for example, gRPC streams. Apps can send periodic
PING frames in Kestrel by setting limits on KestrelServerOptions:

C#

public static IHostBuilder CreateHostBuilder(string[] args) =>


Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
options.Limits.Http2.KeepAlivePingInterval =
TimeSpan.FromSeconds(10);
options.Limits.Http2.KeepAlivePingTimeout =
TimeSpan.FromSeconds(1);
});
webBuilder.UseStartup<Startup>();
});

Containers
Prior to .NET 5.0, building and publishing a Dockerfile for an ASP.NET Core app required
pulling the entire .NET Core SDK and the ASP.NET Core image. With this release, pulling
the SDK images bytes is reduced and the bytes pulled for the ASP.NET Core image is
largely eliminated. For more information, see this GitHub issue comment .
Authentication and authorization

Azure Active Directory authentication with


Microsoft.Identity.Web
The ASP.NET Core project templates now integrate with Microsoft.Identity.Web to
handle authentication with Azure Active Directory (Azure AD). The
Microsoft.Identity.Web package provides:

A better experience for authentication through Azure AD.


An easier way to access Azure resources on behalf of your users, including
Microsoft Graph. See the Microsoft.Identity.Web sample , which starts with a
basic login and advances through multi-tenancy, using Azure APIs, using Microsoft
Graph, and protecting your own APIs. Microsoft.Identity.Web is available
alongside .NET 5.

Allow anonymous access to an endpoint


The AllowAnonymous extension method allows anonymous access to an endpoint:

C#

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
})
.AllowAnonymous();
});
}

Custom handling of authorization failures


Custom handling of authorization failures is now easier with the new
IAuthorizationMiddlewareResultHandler interface that is invoked by the authorization
Middleware. The default implementation remains the same, but a custom handler can
be registered in [Dependency injection, which allows custom HTTP responses based on
why authorization failed. See this sample that demonstrates usage of the
IAuthorizationMiddlewareResultHandler .

Authorization when using endpoint routing


Authorization when using endpoint routing now receives the HttpContext rather than
the endpoint instance. This allows the authorization middleware to access the RouteData
and other properties of the HttpContext that were not accessible though the Endpoint
class. The endpoint can be fetched from the context using context.GetEndpoint.

Role-based access control with Kerberos authentication


and LDAP on Linux
See Kerberos authentication and role-based access control (RBAC)

API improvements

JSON extension methods for HttpRequest and


HttpResponse
JSON data can be read and written to from an HttpRequest and HttpResponse using the
new ReadFromJsonAsync and WriteAsJsonAsync extension methods. These extension
methods use the System.Text.Json serializer to handle the JSON data. The new
HasJsonContentType extension method can also check if a request has a JSON content

type.

The JSON extension methods can be combined with endpoint routing to create JSON
APIs in a style of programming we call route to code. It is a new option for developers
who want to create basic JSON APIs in a lightweight way. For example, a web app that
has only a handful of endpoints might choose to use route to code rather than the full
functionality of ASP.NET Core MVC:

C#

endpoints.MapGet("/weather/{city:alpha}", async context =>


{
var city = (string)context.Request.RouteValues["city"];
var weather = GetFromDatabase(city);
await context.Response.WriteAsJsonAsync(weather);
});

System.Diagnostics.Activity
The default format for System.Diagnostics.Activity now defaults to the W3C format. This
makes distributed tracing support in ASP.NET Core interoperable with more frameworks
by default.

FromBodyAttribute
FromBodyAttribute now supports configuring an option that allows these parameters or
properties to be considered optional:

C#

public IActionResult Post([FromBody(EmptyBodyBehavior =


EmptyBodyBehavior.Allow)]
MyModel model)
{
...
}

Miscellaneous improvements
We’ve started applying nullable annotations to ASP.NET Core assemblies. We plan to
annotate most of the common public API surface of the .NET 5 framework.

Control Startup class activation


An additional UseStartup overload has been added that lets an app provide a factory
method for controlling Startup class activation. Controlling Startup class activation is
useful to pass additional parameters to Startup that are initialized along with the host:

C#

public class Program


{
public static async Task Main(string[] args)
{
var logger = CreateLogger();
var host = Host.CreateDefaultBuilder()
.ConfigureWebHost(builder =>
{
builder.UseStartup(context => new Startup(logger));
})
.Build();

await host.RunAsync();
}
}

Auto refresh with dotnet watch


In .NET 5, running dotnet watch on an ASP.NET Core project both launches the default
browser and auto refreshes the browser as changes are made to the code. This means
you can:

Open an ASP.NET Core project in a text editor.


Run dotnet watch .
Focus on the code changes while the tooling handles rebuilding, restarting, and
reloading the app.

Console Logger Formatter


Improvements have been made to the console log provider in the
Microsoft.Extensions.Logging library. Developers can now implement a custom

ConsoleFormatter to exercise complete control over formatting and colorization of the

console output. The formatter APIs allow for rich formatting by implementing a subset
of the VT-100 escape sequences. VT-100 is supported by most modern terminals. The
console logger can parse out escape sequences on unsupported terminals allowing
developers to author a single formatter for all terminals.

JSON Console Logger


In addition to support for custom formatters, we’ve also added a built-in JSON formatter
that emits structured JSON logs to the console. The following code shows how to switch
from the default logger to JSON:

C#

public static IHostBuilder CreateHostBuilder(string[] args) =>


Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.AddJsonConsole(options =>
{
options.JsonWriterOptions = new JsonWriterOptions()
{ Indented = true };
});
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});

Log messages emitted to the console are JSON formatted:

JSON

{
"EventId": 0,
"LogLevel": "Information",
"Category": "Microsoft.Hosting.Lifetime",
"Message": "Now listening on: https://localhost:5001",
"State": {
"Message": "Now listening on: https://localhost:5001",
"address": "https://localhost:5001",
"{OriginalFormat}": "Now listening on: {address}"
}
}
What's new in ASP.NET Core 3.1
Article • 06/03/2022

This article highlights the most significant changes in ASP.NET Core 3.1 with links to
relevant documentation.

Partial class support for Razor components


Razor components are now generated as partial classes. Code for a Razor component
can be written using a code-behind file defined as a partial class rather than defining all
the code for the component in a single file. For more information, see Partial class
support.

Component Tag Helper and pass parameters to


top-level components
In Blazor with ASP.NET Core 3.0, components were rendered into pages and views using
an HTML Helper ( Html.RenderComponentAsync ). In ASP.NET Core 3.1, render a component
from a page or view with the new Component Tag Helper:

CSHTML

<component type="typeof(Counter)" render-mode="ServerPrerendered" />

The HTML Helper remains supported in ASP.NET Core 3.1, but the Component Tag
Helper is recommended.

Blazor Server apps can now pass parameters to top-level components during the initial
render. Previously you could only pass parameters to a top-level component with
RenderMode.Static. With this release, both RenderMode.Server and
RenderMode.ServerPrerendered are supported. Any specified parameter values are
serialized as JSON and included in the initial response.

For example, prerender a Counter component with an increment amount


( IncrementAmount ):

CSHTML

<component type="typeof(Counter)" render-mode="ServerPrerendered"


param-IncrementAmount="10" />
For more information, see Integrate components into Razor Pages and MVC apps.

Support for shared queues in HTTP.sys


HTTP.sys supports creating anonymous request queues. In ASP.NET Core 3.1, we've
added the ability to create or attach to an existing named HTTP.sys request queue.
Creating or attaching to an existing named HTTP.sys request queue enables scenarios
where the HTTP.sys controller process that owns the queue is independent of the
listener process. This independence makes it possible to preserve existing connections
and enqueued requests between listener process restarts:

C#

public static IHostBuilder CreateHostBuilder(string[] args) =>


Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// ...
webBuilder.UseHttpSys(options =>
{
options.RequestQueueName = "MyExistingQueue";
options.RequestQueueMode = RequestQueueMode.CreateOrAttach;
});
});

Breaking changes for SameSite cookies


The behavior of SameSite cookies has changed to reflect upcoming browser changes.
This may affect authentication scenarios like AzureAd, OpenIdConnect, or WsFederation.
For more information, see Work with SameSite cookies in ASP.NET Core.

Prevent default actions for events in Blazor


apps
Use the @on{EVENT}:preventDefault directive attribute to prevent the default action for
an event. In the following example, the default action of displaying the key's character in
the text box is prevented:

razor

<input value="@_count" @onkeypress="KeyHandler" @onkeypress:preventDefault


/>
For more information, see Prevent default actions.

Stop event propagation in Blazor apps


Use the @on{EVENT}:stopPropagation directive attribute to stop event propagation. In
the following example, selecting the checkbox prevents click events from the child
<div> from propagating to the parent <div> :

razor

<input @bind="_stopPropagation" type="checkbox" />

<div @onclick="OnSelectParentDiv">
<div @onclick="OnSelectChildDiv"
@onclick:stopPropagation="_stopPropagation">
...
</div>
</div>

@code {
private bool _stopPropagation = false;
}

For more information, see Stop event propagation.

Detailed errors during Blazor app development


When a Blazor app isn't functioning properly during development, receiving detailed
error information from the app assists in troubleshooting and fixing the issue. When an
error occurs, Blazor apps display a gold bar at the bottom of the screen:

During development, the gold bar directs you to the browser console, where you
can see the exception.
In production, the gold bar notifies the user that an error has occurred and
recommends refreshing the browser.

For more information, see Handle errors in ASP.NET Core Blazor apps.
What's new in ASP.NET Core 3.0
Article • 03/17/2023

This article highlights the most significant changes in ASP.NET Core 3.0 with links to
relevant documentation.

Blazor
Blazor is a new framework in ASP.NET Core for building interactive client-side web UI
with .NET:

Create rich interactive UIs using C# instead of JavaScript.


Share server-side and client-side app logic written in .NET.
Render the UI as HTML and CSS for wide browser support, including mobile
browsers.

Blazor framework supported scenarios:

Reusable UI components (Razor components)


Client-side routing
Component layouts
Support for dependency injection
Forms and validation
Supply Razor components in Razor class libraries
JavaScript interop

For more information, see ASP.NET Core Blazor.

Blazor Server
Blazor decouples component rendering logic from how UI updates are applied. Blazor
Server provides support for hosting Razor components on the server in an ASP.NET Core
app. UI updates are handled over a SignalR connection. Blazor Server is supported in
ASP.NET Core 3.0.

Blazor WebAssembly (Preview)


Blazor apps can also be run directly in the browser using a WebAssembly-based .NET
runtime. Blazor WebAssembly is in preview and not supported in ASP.NET Core 3.0.
Blazor WebAssembly will be supported in a future release of ASP.NET Core.
Razor components
Blazor apps are built from components. Components are self-contained chunks of user
interface (UI), such as a page, dialog, or form. Components are normal .NET classes that
define UI rendering logic and client-side event handlers. You can create rich interactive
web apps without JavaScript.

Components in Blazor are typically authored using Razor syntax, a natural blend of
HTML and C#. Razor components are similar to Razor Pages and MVC views in that they
both use Razor. Unlike pages and views, which are based on a request-response model,
components are used specifically for handling UI composition.

gRPC
gRPC :

Is a popular, high-performance RPC (remote procedure call) framework.

Offers an opinionated contract-first approach to API development.

Uses modern technologies such as:


HTTP/2 for transport.
Protocol Buffers as the interface description language.
Binary serialization format.

Provides features such as:


Authentication
Bidirectional streaming and flow control.
Cancellation and timeouts.

gRPC functionality in ASP.NET Core 3.0 includes:

Grpc.AspNetCore : An ASP.NET Core framework for hosting gRPC services. gRPC


on ASP.NET Core integrates with standard ASP.NET Core features like logging,
dependency injection (DI), authentication, and authorization.
Grpc.Net.Client : A gRPC client for .NET Core that builds upon the familiar
HttpClient .
Grpc.Net.ClientFactory : gRPC client integration with HttpClientFactory .

For more information, see Overview for gRPC on .NET.

SignalR
See Update SignalR code for migration instructions. SignalR now uses System.Text.Json
to serialize/deserialize JSON messages. See Switch to Newtonsoft.Json for instructions to
restore the Newtonsoft.Json -based serializer.

In the JavaScript and .NET Clients for SignalR, support was added for automatic
reconnection. By default, the client tries to reconnect immediately and retry after 2, 10,
and 30 seconds if necessary. If the client successfully reconnects, it receives a new
connection ID. Automatic reconnect is opt-in:

JavaScript

const connection = new signalR.HubConnectionBuilder()


.withUrl("/chathub")
.withAutomaticReconnect()
.build();

The reconnection intervals can be specified by passing an array of millisecond-based


durations:

JavaScript

.withAutomaticReconnect([0, 3000, 5000, 10000, 15000, 30000])


//.withAutomaticReconnect([0, 2000, 10000, 30000]) The default intervals.

A custom implementation can be passed in for full control of the reconnection intervals.

If the reconnection fails after the last reconnect interval:

The client considers the connection is offline.


The client stops trying to reconnect.

During reconnection attempts, update the app UI to notify the user that the
reconnection is being attempted.

To provide UI feedback when the connection is interrupted, the SignalR client API has
been expanded to include the following event handlers:

onreconnecting : Gives developers an opportunity to disable UI or to let users know

the app is offline.


onreconnected : Gives developers an opportunity to update the UI once the

connection is reestablished.

The following code uses onreconnecting to update the UI while trying to connect:

JavaScript
connection.onreconnecting((error) => {
const status = `Connection lost due to error "${error}". Reconnecting.`;
document.getElementById("messageInput").disabled = true;
document.getElementById("sendButton").disabled = true;
document.getElementById("connectionStatus").innerText = status;
});

The following code uses onreconnected to update the UI on connection:

JavaScript

connection.onreconnected((connectionId) => {
const status = `Connection reestablished. Connected.`;
document.getElementById("messageInput").disabled = false;
document.getElementById("sendButton").disabled = false;
document.getElementById("connectionStatus").innerText = status;
});

SignalR 3.0 and later provides a custom resource to authorization handlers when a hub
method requires authorization. The resource is an instance of HubInvocationContext .
The HubInvocationContext includes the:

HubCallerContext

Name of the hub method being invoked.


Arguments to the hub method.

Consider the following example of a chat room app allowing multiple organization sign-
in via Azure Active Directory. Anyone with a Microsoft account can sign in to chat, but
only members of the owning organization can ban users or view users' chat histories.
The app could restrict certain functionality from specific users.

C#

public class DomainRestrictedRequirement :


AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
IAuthorizationRequirement
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
DomainRestrictedRequirement requirement,
HubInvocationContext resource)
{
if (context.User?.Identity?.Name == null)
{
return Task.CompletedTask;
}

if (IsUserAllowedToDoThis(resource.HubMethodName,
context.User.Identity.Name))
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}

private bool IsUserAllowedToDoThis(string hubMethodName, string


currentUsername)
{
if (hubMethodName.Equals("banUser",
StringComparison.OrdinalIgnoreCase))
{
return currentUsername.Equals("[email protected]",
StringComparison.OrdinalIgnoreCase);
}

return currentUsername.EndsWith("@jabbr.net",
StringComparison.OrdinalIgnoreCase));
}
}

In the preceding code, DomainRestrictedRequirement serves as a custom


IAuthorizationRequirement . Because the HubInvocationContext resource parameter is

being passed in, the internal logic can:

Inspect the context in which the Hub is being called.


Make decisions on allowing the user to execute individual Hub methods.

Individual Hub methods can be marked with the name of the policy the code checks at
run-time. As clients attempt to call individual Hub methods, the
DomainRestrictedRequirement handler runs and controls access to the methods. Based
on the way the DomainRestrictedRequirement controls access:

All logged-in users can call the SendMessage method.


Only users who have logged in with a @jabbr.net email address can view users'
histories.
Only [email protected] can ban users from the chat room.

C#

[Authorize]
public class ChatHub : Hub
{
public void SendMessage(string message)
{
}
[Authorize("DomainRestricted")]
public void BanUser(string username)
{
}

[Authorize("DomainRestricted")]
public void ViewUserHistory(string username)
{
}
}

Creating the DomainRestricted policy might involve:

In Startup.cs , adding the new policy.


Provide the custom DomainRestrictedRequirement requirement as a parameter.
Registering DomainRestricted with the authorization middleware.

C#

services
.AddAuthorization(options =>
{
options.AddPolicy("DomainRestricted", policy =>
{
policy.Requirements.Add(new DomainRestrictedRequirement());
});
});

SignalR hubs use Endpoint Routing. SignalR hub connection was previously done
explicitly:

C#

app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("hubs/chat");
});

In the previous version, developers needed to wire up controllers, Razor pages, and
hubs in a variety of places. Explicit connection results in a series of nearly-identical
routing segments:

C#

app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("hubs/chat");
});
app.UseRouting(routes =>
{
routes.MapRazorPages();
});

SignalR 3.0 hubs can be routed via endpoint routing. With endpoint routing, typically all
routing can be configured in UseRouting :

C#

app.UseRouting(routes =>
{
routes.MapRazorPages();
routes.MapHub<ChatHub>("hubs/chat");
});

ASP.NET Core 3.0 SignalR added:

Client-to-server streaming. With client-to-server streaming, server-side methods can


take instances of either an IAsyncEnumerable<T> or ChannelReader<T> . In the following
C# sample, the UploadStream method on the Hub will receive a stream of strings from
the client:

C#

public async Task UploadStream(IAsyncEnumerable<string> stream)


{
await foreach (var item in stream)
{
// process content
}
}

.NET client apps can pass either an IAsyncEnumerable<T> or ChannelReader<T> instance


as the stream argument of the UploadStream Hub method above.

After the for loop has completed and the local function exits, the stream completion is
sent:

C#

async IAsyncEnumerable<string> clientStreamData()


{
for (var i = 0; i < 5; i++)
{
var data = await FetchSomeData();
yield return data;
}
}

await connection.SendAsync("UploadStream", clientStreamData());

JavaScript client apps use the SignalR Subject (or an RxJS Subject ) for the stream
argument of the UploadStream Hub method above.

JavaScript

let subject = new signalR.Subject();


await connection.send("StartStream", "MyAsciiArtStream", subject);

The JavaScript code could use the subject.next method to handle strings as they are
captured and ready to be sent to the server.

JavaScript

subject.next("example");
subject.complete();

Using code like the two preceding snippets, real-time streaming experiences can be
created.

New JSON serialization


ASP.NET Core 3.0 now uses System.Text.Json by default for JSON serialization:

Reads and writes JSON asynchronously.


Is optimized for UTF-8 text.
Typically higher performance than Newtonsoft.Json .

To add Json.NET to ASP.NET Core 3.0, see Add Newtonsoft.Json-based JSON format
support.

New Razor directives


The following list contains new Razor directives:

@attribute: The @attribute directive applies the given attribute to the class of the
generated page or view. For example, @attribute [Authorize] .
@implements: The @implements directive implements an interface for the
generated class. For example, @implements IDisposable .

IdentityServer4 supports authentication and


authorization for web APIs and SPAs
ASP.NET Core 3.0 offers authentication in Single Page Apps (SPAs) using the support for
web API authorization. ASP.NET Core Identity for authenticating and storing users is
combined with IdentityServer4 for implementing OpenID Connect.

IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core 3.0. It
enables the following security features:

Authentication as a Service (AaaS)


Single sign-on/off (SSO) over multiple application types
Access control for APIs
Federation Gateway

For more information, see the IdentityServer4 documentation or Authentication and


authorization for SPAs.

Certificate and Kerberos authentication


Certificate authentication requires:

Configuring the server to accept certificates.


Adding the authentication middleware in Startup.Configure .
Adding the certificate authentication service in Startup.ConfigureServices .

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate();
// Other service configuration removed.
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseAuthentication();
// Other app configuration removed.
}
Options for certificate authentication include the ability to:

Accept self-signed certificates.


Check for certificate revocation.
Check that the proffered certificate has the right usage flags in it.

A default user principal is constructed from the certificate properties. The user principal
contains an event that enables supplementing or replacing the principal. For more
information, see Configure certificate authentication in ASP.NET Core.

Windows Authentication has been extended onto Linux and macOS. In previous
versions, Windows Authentication was limited to IIS and HTTP.sys. In ASP.NET Core 3.0,
Kestrel has the ability to use Negotiate, Kerberos, and NTLM on Windows, Linux, and
macOS for Windows domain-joined hosts. Kestrel support of these authentication
schemes is provided by the Microsoft.AspNetCore.Authentication.Negotiate NuGet
package. As with the other authentication services, configure authentication app wide,
then configure the service:

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();
// Other service configuration removed.
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseAuthentication();
// Other app configuration removed.
}

Host requirements:

Windows hosts must have Service Principal Names (SPNs) added to the user
account hosting the app.
Linux and macOS machines must be joined to the domain.
SPNs must be created for the web process.
Keytab files must be generated and configured on the host machine.

For more information, see Configure Windows Authentication in ASP.NET Core.

Template changes
The web UI templates (Razor Pages, MVC with controller and views) have the following
removed:

The cookie consent UI is no longer included. To enable the cookie consent feature
in an ASP.NET Core 3.0 template-generated app, see General Data Protection
Regulation (GDPR) support in ASP.NET Core.
Scripts and related static assets are now referenced as local files instead of using
CDNs. For more information, see Scripts and related static assets are now
referenced as local files instead of using CDNs based on the current environment
(dotnet/AspNetCore.Docs #14350) .

The Angular template updated to use Angular 8.

The Razor class library (RCL) template defaults to Razor component development by
default. A new template option in Visual Studio provides template support for pages
and views. When creating an RCL from the template in a command shell, pass the --
support-pages-and-views option ( dotnet new razorclasslib --support-pages-and-views ).

Generic Host
The ASP.NET Core 3.0 templates use .NET Generic Host in ASP.NET Core. Previous
versions used WebHostBuilder. Using the .NET Core Generic Host (HostBuilder) provides
better integration of ASP.NET Core apps with other server scenarios that aren't web-
specific. For more information, see HostBuilder replaces WebHostBuilder.

Host configuration
Prior to the release of ASP.NET Core 3.0, environment variables prefixed with
ASPNETCORE_ were loaded for host configuration of the Web Host. In 3.0,
AddEnvironmentVariables is used to load environment variables prefixed with DOTNET_

for host configuration with CreateDefaultBuilder .

Changes to Startup constructor injection


The Generic Host only supports the following types for Startup constructor injection:

IHostEnvironment
IWebHostEnvironment

IConfiguration
All services can still be injected directly as arguments to the Startup.Configure method.
For more information, see Generic Host restricts Startup constructor injection
(aspnet/Announcements #353) .

Kestrel
Kestrel configuration has been updated for the migration to the Generic Host. In
3.0, Kestrel is configured on the web host builder provided by
ConfigureWebHostDefaults .

Connection Adapters have been removed from Kestrel and replaced with
Connection Middleware, which is similar to HTTP Middleware in the ASP.NET Core
pipeline but for lower-level connections.
The Kestrel transport layer has been exposed as a public interface in
Connections.Abstractions .

Ambiguity between headers and trailers has been resolved by moving trailing
headers to a new collection.
Synchronous I/O APIs, such as HttpRequest.Body.Read , are a common source of
thread starvation leading to app crashes. In 3.0, AllowSynchronousIO is disabled by
default.

For more information, see Migrate from ASP.NET Core 2.2 to 3.0.

HTTP/2 enabled by default


HTTP/2 is enabled by default in Kestrel for HTTPS endpoints. HTTP/2 support for IIS or
HTTP.sys is enabled when supported by the operating system.

EventCounters on request
The Hosting EventSource, Microsoft.AspNetCore.Hosting , emits the following new
EventCounter types related to incoming requests:

requests-per-second

total-requests
current-requests

failed-requests

Endpoint routing
Endpoint Routing, which allows frameworks (for example, MVC) to work well with
middleware, is enhanced:

The order of middleware and endpoints is configurable in the request processing


pipeline of Startup.Configure .
Endpoints and middleware compose well with other ASP.NET Core-based
technologies, such as Health Checks.
Endpoints can implement a policy, such as CORS or authorization, in both
middleware and MVC.
Filters and attributes can be placed on methods in controllers.

For more information, see Routing in ASP.NET Core.

Health Checks
Health Checks use endpoint routing with the Generic Host. In Startup.Configure , call
MapHealthChecks on the endpoint builder with the endpoint URL or relative path:

C#

app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
});

Health Checks endpoints can:

Specify one or more permitted hosts/ports.


Require authorization.
Require CORS.

For more information, see the following articles:

Migrate from ASP.NET Core 2.2 to 3.0


Health checks in ASP.NET Core

Pipes on HttpContext
It's now possible to read the request body and write the response body using the
System.IO.Pipelines API. The HttpRequest.BodyReader property provides a PipeReader
that can be used to read the request body. The HttpResponse.BodyWriter property
provides a PipeWriter that can be used to write the response body.
HttpRequest.BodyReader is an analogue of the HttpRequest.Body stream.

HttpResponse.BodyWriter is an analogue of the HttpResponse.Body stream.

Improved error reporting in IIS


Startup errors when hosting ASP.NET Core apps in IIS now produce richer diagnostic
data. These errors are reported to the Windows Event Log with stack traces wherever
applicable. In addition, all warnings, errors, and unhandled exceptions are logged to the
Windows Event Log.

Worker Service and Worker SDK


.NET Core 3.0 introduces the new Worker Service app template. This template provides a
starting point for writing long running services in .NET Core.

For more information, see:

.NET Core Workers as Windows Services


Background tasks with hosted services in ASP.NET Core
Host ASP.NET Core in a Windows Service

Forwarded Headers Middleware improvements


In previous versions of ASP.NET Core, calling UseHsts and UseHttpsRedirection were
problematic when deployed to an Azure Linux or behind any reverse proxy other than
IIS. The fix for previous versions is documented in Forward the scheme for Linux and
non-IIS reverse proxies.

This scenario is fixed in ASP.NET Core 3.0. The host enables the Forwarded Headers
Middleware when the ASPNETCORE_FORWARDEDHEADERS_ENABLED environment variable is set
to true . ASPNETCORE_FORWARDEDHEADERS_ENABLED is set to true in our container images.

Performance improvements
ASP.NET Core 3.0 includes many improvements that reduce memory usage and improve
throughput:

Reduction in memory usage when using the built-in dependency injection


container for scoped services.
Reduction in allocations across the framework, including middleware scenarios and
routing.
Reduction in memory usage for WebSocket connections.
Memory reduction and throughput improvements for HTTPS connections.
New optimized and fully asynchronous JSON serializer.
Reduction in memory usage and throughput improvements in form parsing.

ASP.NET Core 3.0 only runs on .NET Core 3.0


As of ASP.NET Core 3.0, .NET Framework is no longer a supported target framework.
Projects targeting .NET Framework can continue in a fully supported fashion using the
.NET Core 2.1 LTS release . Most ASP.NET Core 2.1.x related packages will be supported
indefinitely, beyond the three-year LTS period for .NET Core 2.1.

For migration information, see Port your code from .NET Framework to .NET Core.

Use the ASP.NET Core shared framework


The ASP.NET Core 3.0 shared framework, contained in the Microsoft.AspNetCore.App
metapackage, no longer requires an explicit <PackageReference /> element in the
project file. The shared framework is automatically referenced when using the
Microsoft.NET.Sdk.Web SDK in the project file:

XML

<Project Sdk="Microsoft.NET.Sdk.Web">

Assemblies removed from the ASP.NET Core


shared framework
The most notable assemblies removed from the ASP.NET Core 3.0 shared framework
are:

Newtonsoft.Json (Json.NET). To add Json.NET to ASP.NET Core 3.0, see Add


Newtonsoft.Json-based JSON format support. ASP.NET Core 3.0 introduces
System.Text.Json for reading and writing JSON. For more information, see New

JSON serialization in this document.


Entity Framework Core
For a complete list of assemblies removed from the shared framework, see Assemblies
being removed from Microsoft.AspNetCore.App 3.0 . For more information on the
motivation for this change, see Breaking changes to Microsoft.AspNetCore.App in 3.0
and A first look at changes coming in ASP.NET Core 3.0 .
What's new in ASP.NET Core 2.2
Article • 06/03/2022

This article highlights the most significant changes in ASP.NET Core 2.2, with links to
relevant documentation.

OpenAPI Analyzers & Conventions


OpenAPI (formerly known as Swagger) is a language-agnostic specification for
describing REST APIs. The OpenAPI ecosystem has tools that allow for discovering,
testing, and producing client code using the specification. Support for generating and
visualizing OpenAPI documents in ASP.NET Core MVC is provided via community driven
projects such as NSwag and Swashbuckle.AspNetCore . ASP.NET Core 2.2 provides
improved tooling and runtime experiences for creating OpenAPI documents.

For more information, see the following resources:

Use web API analyzers


Use web API conventions
ASP.NET Core 2.2.0-preview1: OpenAPI Analyzers & Conventions

Problem details support


ASP.NET Core 2.1 introduced ProblemDetails , based on the RFC 7807 specification for
carrying details of an error with an HTTP Response. In 2.2, ProblemDetails is the
standard response for client error codes in controllers attributed with
ApiControllerAttribute . An IActionResult returning a client error status code (4xx)
now returns a ProblemDetails body. The result also includes a correlation ID that can be
used to correlate the error using request logs. For client errors, ProducesResponseType
defaults to using ProblemDetails as the response type. This is documented in OpenAPI /
Swagger output generated using NSwag or Swashbuckle.AspNetCore.

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. Notable features of HTTP/2 include:

Support for header compression.


Fully multiplexed streams over a single connection.

While HTTP/2 preserves HTTP's semantics (for example, HTTP headers and methods), it's
a breaking change from HTTP/1.x on how data is framed and sent between the client
and server.

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:

Mitigate UseIIS conflict


Configure Kestrel server options with ConfigureKestrel

IIS in-process hosting


In earlier versions of ASP.NET Core, IIS serves as a reverse proxy. In 2.2, the ASP.NET
Core Module can boot the CoreCLR and host an app inside the IIS worker process
(w3wp.exe). In-process hosting provides performance and diagnostic gains when
running with IIS.

For more information, see in-process hosting for IIS.

SignalR Java client


ASP.NET Core 2.2 introduces a Java Client for SignalR. This client supports connecting to
an ASP.NET Core SignalR Server from Java code, including Android apps.

For more information, see ASP.NET Core SignalR Java client.

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.

HTTP Client performance


In ASP.NET Core 2.2, the performance of SocketsHttpHandler was improved by reducing
connection pool locking contention. For apps that make many outgoing HTTP requests,
such as some microservices architectures, throughput is improved. Under load,
HttpClient throughput can be improved by up to 60% on Linux and 20% on Windows.

For more information, see the pull request that made this improvement .

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
Article • 02/07/2023

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.

Razor class libraries


ASP.NET Core 2.1 makes it easier to build and include Razor-based UI in a library and
share it across multiple projects. The new Razor SDK enables building Razor files into a
class library project that can be packaged into a NuGet package. Views and pages in
libraries are automatically discovered and can be overridden by the app. By integrating
Razor compilation into the build:

The app startup time is significantly faster.


Fast updates to Razor views and pages at runtime are still available as part of an
iterative development workflow.

For more information, see Create reusable UI using the Razor Class Library project.

Identity UI library & scaffolding


ASP.NET Core 2.1 provides ASP.NET Core Identity as a Razor Class Library. Apps that
include Identity can apply the new Identity scaffolder to selectively add the source code
contained in the Identity Razor Class Library (RCL). You might want to generate source
code so you can modify the code and change the behavior. For example, you could
instruct the scaffolder to generate the code used in registration. Generated code takes
precedence over the same code in the Identity RCL.

Apps that do not include authentication can apply the Identity scaffolder to add the RCL
Identity package. You have the option of selecting Identity code to be generated.

For more information, see Scaffold Identity in ASP.NET Core projects.

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<TEntryPoint> 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:

C#

public class BasicTests


: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly HttpClient _client;

public BasicTests(WebApplicationFactory<RazorPagesProject.Startup>
factory)
{
_client = factory.CreateClient();
}

[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());
}
}

For more information, see the Integration tests topic.

[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.

Kestrel libuv transport configuration


With the release of ASP.NET Core 2.1, Kestrel's default transport is no longer based on
Libuv but instead based on managed sockets. For more information, see Kestrel web
server implementation: Libuv transport configuration.

Generic host builder


The Generic Host Builder ( HostBuilder ) has been introduced. This builder can be used
for apps that don't process HTTP requests (Messaging, background tasks, etc.).

For more information, see .NET Generic Host.

Updated SPA templates


The Single Page Application templates for Angular and React are updated to use the
standard project structures and build systems for each framework.

The Angular template is based on the Angular CLI, and the React template is based on
create-react-app.

For more information, see:

Use Angular with ASP.NET Core


Use React with ASP.NET Core

Razor Pages search for Razor assets


In 2.1, Razor Pages search for Razor assets (such as layouts and partials) in the following
directories in the listed order:

1. Current Pages folder.


2. /Pages/Shared/
3. /Views/Shared/

Razor Pages in an area


Razor Pages now support areas. To see an example of areas, create a new Razor Pages
web app with individual user accounts. A Razor Pages web app with individual user
accounts includes /Areas/Identity/Pages.

MVC compatibility version


The SetCompatibilityVersion method allows an app to opt-in or opt-out of potentially
breaking behavior changes introduced in ASP.NET Core MVC 2.1 or later.

For more information, see Compatibility version for ASP.NET Core MVC.

Migrate from 2.0 to 2.1


See Migrate from ASP.NET Core 2.0 to 2.1.

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
Article • 06/03/2022

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

ASP.NET Core metapackage


A new ASP.NET Core metapackage includes all of the packages made and supported by
the ASP.NET Core and Entity Framework Core teams, along with their internal and 3rd-
party dependencies. You no longer need to choose individual ASP.NET Core features by
package. All features are included in the Microsoft.AspNetCore.All package. The
default templates use this package.

For more information, see Microsoft.AspNetCore.All metapackage for ASP.NET Core 2.0.

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

.NET Standard 2.0


The ASP.NET Core 2.0 packages target .NET Standard 2.0. The packages can be
referenced by other .NET Standard 2.0 libraries, and they can run on .NET Standard 2.0-
compliant implementations of .NET, including .NET Core 2.0 and .NET Framework 4.6.1.

The Microsoft.AspNetCore.All metapackage targets .NET Core 2.0 only, because it's
intended to be used with the .NET Core 2.0 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 The features described in this article are
obsolete as of ASP.NET Core 3.0.

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.

WebListener renamed to HTTP.sys


The packages Microsoft.AspNetCore.Server.WebListener and
Microsoft.Net.Http.Server have been merged into a new package

Microsoft.AspNetCore.Server.HttpSys . The namespaces have been updated to match.

For more information, see HTTP.sys web server implementation in ASP.NET Core.

Enhanced HTTP header support


When using MVC to transmit a FileStreamResult or a FileContentResult , you now have
the option to set an ETag or a LastModified date on the content you transmit. You can
set these values on the returned content with code similar to the following:
C#

var data = Encoding.UTF8.GetBytes("This is a sample text from a binary


array");
var entityTag = new EntityTagHeaderValue("\"MyCalculatedEtagValue\"");
return File(data, "text/plain", "downloadName.txt", lastModified:
DateTime.UtcNow.AddSeconds(-5), entityTag: entityTag);

The file returned to your visitors has 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.

Hosting startup and Application Insights


Hosting environments can now inject extra package dependencies and execute code
during application startup, without the application needing to explicitly take a
dependency or call any methods. This feature can be used to enable certain
environments to "light-up" features unique to that environment without the application
needing to know ahead of time.

In ASP.NET Core 2.0, this feature is used to automatically enable Application Insights
diagnostics when debugging in Visual Studio and (after opting in) when running in
Azure App Services. As a result, the project templates no longer add Application Insights
packages and code by default.

For information about the status of planned documentation, see the GitHub issue .

Automatic use of anti-forgery tokens


ASP.NET Core has always helped HTML-encode content by default, but with the new
version an extra step is taken to help prevent cross-site request forgery (XSRF) attacks.
ASP.NET Core will now emit anti-forgery tokens by default and validate them on form
POST actions and pages without extra configuration.

For more information, see Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in
ASP.NET Core.
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.

Razor support for C# 7.1


The Razor view engine has been updated to work with the new Roslyn compiler. That
includes support for C# 7.1 features like Default Expressions, Inferred Tuple Names, and
Pattern-Matching with Generics. To use C# 7.1 in your project, add the following
property in your project file and then reload the solution:

XML

<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
Article • 06/03/2022

ASP.NET Core 1.1 includes the following new features:

URL Rewriting Middleware


Response Caching Middleware
View Components as Tag Helpers
Middleware as MVC filters
Cookie-based TempData provider
Azure App Service logging provider
Azure Key Vault configuration provider
Azure and Redis Storage Data Protection Key Repositories
WebListener Server for Windows
WebSockets support

Choosing between versions 1.0 and 1.1 of


ASP.NET Core
ASP.NET Core 1.1 has more features than ASP.NET Core 1.0. In general, we recommend
you use the latest version.

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 .
Choose an ASP.NET Core web UI
Article • 01/04/2023

ASP.NET Core is a complete UI framework. Choose which functionalities to combine that


fit the app's web UI needs.

Benefits vs. costs of server and client rendered


UI
There are three general approaches to building modern web UI with ASP.NET Core:

Apps that render UI from the server.


Apps that render UI on the client in the browser.
Hybrid apps that take advantage of both server and client UI rendering
approaches. For example, most of the web UI is rendered on the server, and client
rendered components are added as needed.

There are benefits and drawbacks to consider when rendering UI on the server or on the
client.

Server rendered UI
A web UI app that renders on the server dynamically generates the page's HTML and
CSS on the server in response to a browser request. The page arrives at the client ready
to display.

Benefits:

The client requirements are minimal because the server does the work of logic and
page generation:
Great for low-end devices and low-bandwidth connections.
Allows for a broad range of browser versions at the client.
Quick initial page load times.
Minimal to no JavaScript to pull to the client.
Flexibility of access to protected server resources:
Database access.
Access to secrets, such as values for API calls to Azure storage.
Static site analysis advantages, such as search engine optimization.

Examples of common server rendered web UI app scenarios:


Dynamic sites such as those that provide personalized pages, data, and forms.
Display read-only data such as transaction lists.
Display static blog pages.
A public-facing content management system.

Drawbacks:

The cost of compute and memory use are concentrated on the server, rather than
each client.
User interactions require a round trip to the server to generate UI updates.

Client rendered UI
A client rendered app dynamically renders web UI on the client, directly updating the
browser DOM as necessary.

Benefits:

Allows for rich interactivity that is nearly instant, without requiring a round trip to
the server. UI event handling and logic run locally on the user's device with
minimal latency.
Supports incremental updates, saving partially completed forms or documents
without the user having to select a button to submit a form.
Can be designed to run in a disconnected mode. Updates to the client-side model
are eventually synchronized back to the server once a connection is re-established.
Reduced server load and cost, the work is offloaded to the client. Many client
rendered apps can also be hosted as static websites.
Takes advantage of the capabilities of the user’s device.

Examples of client rendered web UI:

An interactive dashboard.
An app featuring drag-and-drop functionality
A responsive and collaborative social app.

Drawbacks:

Code for the logic has to be downloaded and executed on the client, adding to the
initial load time.
Client requirements may exclude user's who have low-end devices, older browser
versions, or low-bandwidth connections.
Choose a server rendered ASP.NET Core UI
solution
The following section explains the ASP.NET Core web UI server rendered models
available and provides links to get started. ASP.NET Core Razor Pages and ASP.NET Core
MVC are server-based frameworks for building web apps with .NET.

ASP.NET Core Razor Pages


Razor Pages is a page-based model. UI and business logic concerns are kept separate,
but within the page. Razor Pages is the recommended way to create new page-based or
form-based apps for developers new to ASP.NET Core. Razor Pages provides an easier
starting point than ASP.NET Core MVC.

Razor Pages benefits, in addition to the server rendering benefits:

Quickly build and update UI. Code for the page is kept with the page, while
keeping UI and business logic concerns separate.
Testable and scales to large apps.
Keep your ASP.NET Core pages organized in a simpler way than ASP.NET MVC:
View specific logic and view models can be kept together in their own
namespace and directory.
Groups of related pages can be kept in their own namespace and directory.

To get started with your first ASP.NET Core Razor Pages app, see Tutorial: Get started
with Razor Pages in ASP.NET Core. For a complete overview of ASP.NET Core Razor
Pages, its architecture and benefits, see: Introduction to Razor Pages in ASP.NET Core.

ASP.NET Core MVC


ASP.NET MVC renders UI on the server and uses a Model-View-Controller (MVC)
architectural pattern. The MVC pattern separates an app into three main groups of
components: Models, Views, and Controllers. User requests are routed to a controller.
The controller is responsible for working with the model to perform user actions or
retrieve results of queries. The controller chooses the view to display to the user, and
provides it with any model data it requires. Support for Razor Pages is built on ASP.NET
Core MVC.

MVC benefits, in addition to the server rendering benefits:

Based on a scalable and mature model for building large web apps.
Clear separation of concerns for maximum flexibility.
The Model-View-Controller separation of responsibilities ensures that the business
model can evolve without being tightly coupled to low-level implementation
details.

To get started with ASP.NET Core MVC, see Get started with ASP.NET Core MVC. For an
overview of ASP.NET Core MVC's architecture and benefits, see Overview of ASP.NET
Core MVC.

Blazor Server
Blazor is a framework for building interactive client-side web UI with .NET:

Create rich interactive UIs using C# instead of JavaScript .


Share server-side and client-side app logic written in .NET.
Render the UI as HTML and CSS for wide browser support, including mobile
browsers.
Integrate with modern hosting platforms, such as Docker.
Build hybrid desktop and mobile apps with .NET and Blazor.

Using .NET for client-side web development offers the following advantages:

Write code in C# instead of JavaScript.


Leverage the existing .NET ecosystem of .NET libraries.
Share app logic across server and client.
Benefit from .NET's performance, reliability, and security.
Stay productive on Windows, Linux, or macOS with a development environment,
such as Visual Studio or Visual Studio Code .
Build on a common set of languages, frameworks, and tools that are stable,
feature-rich, and easy to use.

Blazor Server provides support for hosting server-rendered UI in an ASP.NET Core app.
Client UI updates are handled over a SignalR connection. The runtime stays on the
server and handles executing the app's C# code.

For more information, see ASP.NET Core Blazor and ASP.NET Core Blazor hosting
models. The client-rendered Blazor hosting model is described in the Blazor
WebAssembly section later in this article.

Choose a client rendered ASP.NET Core solution


The following section briefly explains the ASP.NET Core web UI client rendered models
available and provides links to get started.
Blazor WebAssembly
Blazor WebAssembly is a single-page app (SPA) framework for building interactive
client-side web apps with the general characteristics described in the Blazor Server
section earlier in this article.

Running .NET code inside web browsers is made possible by WebAssembly


(abbreviated wasm ). WebAssembly is a compact bytecode format optimized for fast
download and maximum execution speed. WebAssembly is an open web standard and
supported in web browsers without plugins. Blazor WebAssembly works in all modern
web browsers, including mobile browsers.

When a Blazor WebAssembly app is built and run:

C# code files and Razor files are compiled into .NET assemblies.
The assemblies and the .NET runtime are downloaded to the browser.
Blazor WebAssembly bootstraps the .NET runtime and configures the runtime to
load the assemblies for the app. The Blazor WebAssembly runtime uses JavaScript
interop to handle Document Object Model (DOM) manipulation and browser
API calls.

For more information, see ASP.NET Core Blazor and ASP.NET Core Blazor hosting
models. The server-rendered Blazor hosting model is described in the Blazor Server
section earlier in this article.

ASP.NET Core Single Page Application (SPA) with


JavaScript Frameworks such as Angular and React
Build client-side logic for ASP.NET Core apps using popular JavaScript frameworks, like
Angular or React . ASP.NET Core provides project templates for Angular and React,
and can be used with other JavaScript frameworks as well.

Benefits of ASP.NET Core SPA with JavaScript Frameworks, in addition to the client
rendering benefits previously listed:

The JavaScript runtime environment is already provided with the browser.


Large community and mature ecosystem.
Build client-side logic for ASP.NET Core apps using popular JS frameworks, like
Angular and React.

Downsides:

More coding languages, frameworks, and tools required.


Difficult to share code so some logic may be duplicated.

To get started, see:

Use Angular with ASP.NET Core


Use React with ASP.NET Core

Choose a hybrid solution: ASP.NET Core MVC or


Razor Pages plus Blazor
MVC, Razor Pages, and Blazor are part of the ASP.NET Core framework and are designed
to be used together. Razor components can be integrated into Razor Pages and MVC
apps in a hosted Blazor WebAssembly or Blazor Server solution. When a view or page is
rendered, components can be prerendered at the same time.

Benefits for MVC or Razor Pages plus Blazor, in addition to MVC or Razor Pages benefits:

Prerendering executes Razor components on the server and renders them into a
view or page, which improves the perceived load time of the app.
Add interactivity to existing views or pages with the Component Tag Helper.

To get started with ASP.NET Core MVC or Razor Pages plus Blazor, see Prerender and
integrate ASP.NET Core Razor components.

Next steps
For more information, see:

ASP.NET Core Blazor


ASP.NET Core Blazor hosting models
Prerender and integrate ASP.NET Core Razor components
Compare gRPC services with HTTP APIs
Tutorial: Create a Razor Pages web app
with ASP.NET Core
Article • 05/24/2023

This series of tutorials explains the basics of building a Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers
and views, see Introduction to Razor Pages in ASP.NET Core.

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web
UI solution will best fit your needs, see Choose an ASP.NET Core UI.

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.
Tutorial: Get started with Razor Pages in
ASP.NET Core
Article • 06/08/2023

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 developers who are familiar with controllers
and views, see Introduction to Razor Pages. For a video introduction, see Entity
Framework Core for Beginners .

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web
UI solution will best fit your needs, see Choose an ASP.NET Core UI.

At the end of this tutorial, you'll have a Razor Pages web app that manages a database
of movies.

Prerequisites
Visual Studio

Visual Studio 2022 with the ASP.NET and web development workload.
Create a Razor Pages web app
Visual Studio

Start Visual Studio and select Create a new project.

In the Create a new project dialog, select ASP.NET Core Web App > Next.

In the Configure your new project dialog, enter RazorPagesMovie for Project
name. It's important to name the project RazorPagesMovie, including
matching the capitalization, so the namespaces will match when you copy and
paste example code.

Select Next.

In the Additional information dialog:


Select .NET 7.0 (Standard Term Support).
Verify: Do not use top-level statements is unchecked.

Select Create.
The following starter project is created:
For alternative approaches to create the project, see Create a new project in Visual
Studio.

Run the app


Visual Studio

Select RazorPagesMovie in Solution Explorer, and then press Ctrl+F5 to run


without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to
use SSL:

Select Yes if you trust the IIS Express SSL certificate.

The following dialog is displayed:


Select Yes if you agree to trust the development certificate.

For information on trusting the Firefox browser, see Firefox


SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

Visual Studio:

Runs the app, which launches the Kestrel server.


Launches the default browser at https://localhost:<port> , which displays the
apps UI. <port> is the random port that is assigned when the app was created.

Examine the project files


The following sections contain 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 has HTML markup with C# code using Razor syntax.
A .cshtml.cs file that has 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. _Layout.cshtml 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 assets, like HTML files, JavaScript files, and CSS files. For more
information, see Static files in ASP.NET Core.

appsettings.json

Contains configuration data, like connection strings. For more information, see
Configuration in ASP.NET Core.

Program.cs
Contains the following code:

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
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.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

The following lines of code in this file create a WebApplicationBuilder with


preconfigured defaults, add Razor Pages support to the Dependency Injection (DI)
container, and builds the app:

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();

var app = builder.Build();

The developer exception page is enabled by default and provides helpful information on
exceptions. Production apps should not be run in development mode because the
developer exception page can leak sensitive information.
The following code sets the exception endpoint to /Error and enables HTTP Strict
Transport Security Protocol (HSTS) when the app is not running in development mode:

C#

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
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();
}

For example, the preceding code runs when the app is in production or test mode. For
more information, see Use multiple environments in ASP.NET Core.

The following code enables various Middleware:

app.UseHttpsRedirection(); : Redirects HTTP requests to HTTPS.


app.UseStaticFiles(); : Enables static files, such as HTML, CSS, images, and

JavaScript to be served. For more information, see Static files in ASP.NET Core.
app.UseRouting(); : Adds route matching to the middleware pipeline. For more
information, see Routing in ASP.NET Core
app.MapRazorPages(); : Configures endpoint routing for Razor Pages.
app.UseAuthorization(); : Authorizes a user to access secure resources. This app

doesn't use authorization, therefore this line could be removed.


app.Run(); : Runs the app.

Troubleshooting with the completed sample


If you run into a problem you can't resolve, compare your code to the completed
project. View or download completed project (how to download).

Next steps
Next: Add a model
Part 2, add a model to a Razor Pages
app in ASP.NET Core
Article • 06/08/2023

In this tutorial, classes are added for managing movies in a database. The app's model
classes use Entity Framework Core (EF Core) to work with the database. EF Core is an
object-relational mapper (O/RM) that simplifies data access. You write the model classes
first, and EF Core creates the database.

The model classes are known as POCO classes (from "Plain-Old CLR Objects") because
they don't have a dependency on EF Core. They define the properties of the data that
are stored in the database.

Add a data model


Visual Studio

1. In Solution Explorer, right-click the RazorPagesMovie project > Add > New
Folder. Name the folder Models .

2. Right-click the Models folder. Select Add > Class. Name the class Movie.

3. Add the following properties to the Movie class:

C#

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

The Movie class contains:


The ID field is required by the database for the primary key.

A [DataType] attribute that specifies the type of data in the ReleaseDate


property. With this attribute:
The user isn't required to enter time information in the date field.
Only the date is displayed, not time information.

The question mark after string indicates that the property is nullable. For
more information, see Nullable reference types.

DataAnnotations are covered in a later tutorial.

Build the project to verify there are no compilation errors.

Scaffold the movie model


In this section, the movie model is scaffolded. That is, the scaffolding tool produces
pages for Create, Read, Update, and Delete (CRUD) operations for the movie model.

Visual Studio

1. Create the Pages/Movies folder:


a. Right-click on the Pages folder > Add > New Folder.
b. Name the folder Movies.

2. Right-click on the Pages/Movies folder > Add > New Scaffolded Item.
3. In the Add New Scaffold dialog, select Razor Pages using Entity Framework
(CRUD) > Add.

4. Complete the Add Razor Pages using Entity Framework (CRUD) dialog:
a. In the Model class drop down, select Movie (RazorPagesMovie.Models).
b. In the Data context class row, select the + (plus) sign.
i. In the Add Data Context dialog, the class name
RazorPagesMovie.Data.RazorPagesMovieContext is generated.
ii. In the Database provider drop down, select SQL Server.
c. Select Add.

The appsettings.json file is updated with the connection string used to connect to
a local database.

Files created and updated


The scaffold process creates the following files:

Pages/Movies: Create, Delete, Details, Edit, and Index.


Data/RazorPagesMovieContext.cs

The created files are explained in the next tutorial.

The scaffold process adds the following highlighted code to the Program.cs file:

Visual Studio

C#
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
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.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

The Program.cs changes are explained later in this tutorial.

Create the initial database schema using EF's


migration feature
The migrations feature in Entity Framework Core provides a way to:

Create the initial database schema.


Incrementally update the database schema to keep it in sync with the app's data
model. Existing data in the database is preserved.

Visual Studio
In this section, the Package Manager Console (PMC) window is used to:

Add an initial migration.


Update the database with the initial migration.

1. From the Tools menu, select NuGet Package Manager > Package Manager
Console.

2. In the PMC, enter the following commands:

PowerShell

Add-Migration InitialCreate
Update-Database

The Add-Migration command generates code to create the initial database


schema. The schema is based on the model specified in DbContext . The
InitialCreate argument is used to name the migration. Any name can be
used, but by convention a name is selected that describes the migration.

The Update-Database command runs the Up method in migrations that have


not been applied. In this case, the command runs the Up method in the
Migrations/<time-stamp>_InitialCreate.cs file, which creates the database.
The following warning is displayed, which is addressed in a later step:

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()'.

The data context RazorPagesMovieContext :

Derives from Microsoft.EntityFrameworkCore.DbContext.


Specifies which entities are included in the data model.
Coordinates EF Core functionality, such as Create, Read, Update and Delete, for the
Movie model.

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Data
{
public class RazorPagesMovieContext : DbContext
{
public RazorPagesMovieContext
(DbContextOptions<RazorPagesMovieContext> options)
: base(options)
{
}

public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; } =


default!;
}
}

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 Configuration system reads the
connection string from the appsettings.json file.
Test the app
1. Run the app and append /Movies to the URL in the browser
( http://localhost:port/movies ).

If you receive the following error:

Console

SqlException: Cannot open database "RazorPagesMovieContext-GUID"


requested by the login. The login failed.
Login failed for user 'User-name'.

You missed the migrations step.

2. Test the Create New link.

7 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 .

3. Test the Edit, Details, and Delete links.

The next tutorial explains the files created by scaffolding.

Examine the context registered with dependency


injection
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 via
constructor parameters. The constructor code that gets a database context instance is
shown later in the tutorial.

The scaffolding tool automatically created a database context and registered it with the
dependency injection container. The following highlighted code is added to the
Program.cs file by the scaffolder:

Visual Studio

C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
var app = builder.Build();

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
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.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Troubleshooting with the completed sample


If you run into a problem you can't resolve, compare your code to the completed
project. View or download completed project (how to download).

Next steps
Previous: Get Started Next: Scaffolded Razor Pages
Part 3, scaffolded Razor Pages in
ASP.NET Core
Article • 06/08/2023

By Rick Anderson

This tutorial examines the Razor Pages created by scaffolding in the previous tutorial.

The Create, Delete, Details, and Edit pages


Examine the Pages/Movies/Index.cshtml.cs Page Model:

C#

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies;

public class IndexModel : PageModel


{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)


{
_context = context;
}

public IList<Movie> Movie { get;set; } = default!;

public async Task OnGetAsync()


{
if (_context.Movie != null)
{
Movie = await _context.Movie.ToListAsync();
}
}
}

Razor Pages are derived from PageModel. By convention, the PageModel derived class is
named PageNameModel . For example, the Index page is named IndexModel .

The constructor uses dependency injection to add the RazorPagesMovieContext to the


page:
C#

public class IndexModel : PageModel


{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)


{
_context = context;
}

See Asynchronous code for more information on asynchronous programming with


Entity Framework.

When a GET request is made for the page, the OnGetAsync method returns a list of
movies to the Razor Page. On a Razor Page, OnGetAsync or OnGet is called to initialize
the state of the page. In this case, OnGetAsync gets a list of movies and displays them.

When OnGet returns void or OnGetAsync returns Task , no return statement is used. For
example, examine the Privacy Page:

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesMovie.Pages
{
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;

public PrivacyModel(ILogger<PrivacyModel> logger)


{
_logger = logger;
}

public void OnGet()


{
}
}
}

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:

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

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

Examine the Pages/Movies/Index.cshtml Razor Page:

CSHTML

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
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 directive


The @page Razor directive makes the file an MVC action, which means that it can handle
requests. @page must be the first Razor directive on a page. @page and @model are
examples of transitioning into Razor-specific markup. See Razor syntax for more
information.

The @model directive


CSHTML

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

The @model directive specifies the type of the model passed to the Razor Page. In the
preceding example, the @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.

Examine the lambda expression used in the following HTML Helper:


CSHTML

@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] is 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 layout page


Select the menu links RazorPagesMovie, Home, and Privacy. Each page shows the same
menu layout. The menu layout is implemented in the Pages/Shared/_Layout.cshtml file.

Open and examine the Pages/Shared/_Layout.cshtml file.

Layout templates allow the HTML container layout to be:

Specified in one place.


Applied in multiple pages in the site.

Find the @RenderBody() line. RenderBody is a placeholder where all the page-specific
views show up, wrapped in the layout page. For example, select the Privacy link and the
Pages/Privacy.cshtml view is rendered inside the RenderBody method.

ViewData and layout


Consider the following markup from the Pages/Movies/Index.cshtml file:

CSHTML

@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
pass data 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.

CSHTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"
/>
<link rel="stylesheet" href="~/RazorPagesMovie.styles.css" asp-append-
version="true" />

The line @*Markup removed for brevity.*@ is a Razor comment. Unlike HTML comments
<!-- --> , Razor comments are not sent to the client. See MDN web docs: Getting

started with HTML for more information.

Update the layout


1. Change the <title> element in the Pages/Shared/_Layout.cshtml file to display
Movie rather than RazorPagesMovie.

CSHTML

<!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>

2. Find the following anchor element in the Pages/Shared/_Layout.cshtml file.

CSHTML
<a class="navbar-brand" asp-area="" asp-
page="/Index">RazorPagesMovie</a>

3. Replace the preceding element with the following markup:

CSHTML

<a class="navbar-brand" asp-page="/Movies/Index">RpMovie</a>

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.

4. Save the changes and test the app by selecting the RpMovie link. See the
_Layout.cshtml file in GitHub if you have any problems.

5. Test the Home, RpMovie, Create, Edit, and Delete links. 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.

7 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 the app.
See this GitHub issue 4076 for instructions on adding decimal comma.

The Layout property is set in the Pages/_ViewStart.cshtml file:

CSHTML

@{
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:

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext
_context;

public CreateModel(RazorPagesMovie.Data.RazorPagesMovieContext
context)
{
_context = context;
}

public IActionResult OnGet()


{
return Page();
}

[BindProperty]
public Movie Movie { get; set; } = default!;

// To protect from overposting attacks, see


https://aka.ms/RazorPagesCRUD
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid || _context.Movie == null || Movie ==
null)
{
return Page();
}

_context.Movie.Add(Movie);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}
}

The OnGet method initializes any state needed for the page. The Create page doesn't
have any state to initialize, so 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:

C#

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.


The browser is redirected to the Index page.

The Create Razor Page


Examine the Pages/Movies/Create.cshtml Razor Page file:

CSHTML

@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 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:

CSHTML

<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 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.

Next steps
Previous: Add a model Next: Work with a database
Part 4 of tutorial series on Razor Pages
Article • 06/08/2023

By Joe Audette

The RazorPagesMovieContext 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 Program.cs :

Visual Studio

C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

The ASP.NET Core Configuration system reads the ConnectionString key. For local
development, configuration gets the connection string from the appsettings.json file.

Visual Studio

The generated connection string is similar to the following JSON:

JSON

{
"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 test or production database server. For more
information, see Configuration.

Visual Studio

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.

1. From the View menu, open SQL Server Object Explorer (SSOX).
2. Right-click on the Movie table and select View Designer:

Note the key icon next to ID . By default, EF creates a property named ID for
the primary key.

3. Right-click on the Movie table and select View Data:


Seed the database
Create a new class named SeedData in the Models folder with the following code:

C#

using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;

namespace RazorPagesMovie.Models;

public static class SeedData


{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new RazorPagesMovieContext(
serviceProvider.GetRequiredService<
DbContextOptions<RazorPagesMovieContext>>()))
{
if (context == null || context.Movie == null)
{
throw new ArgumentNullException("Null
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 database, the seed initializer returns and no movies are
added.

C#

if (context.Movie.Any())
{
return;
}

Add the seed initializer


Update the Program.cs with the following highlighted code:

Visual Studio
C#

using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())


{
var services = scope.ServiceProvider;

SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

In the previous code, Program.cs has been modified to do the following:

Get a database context instance from the dependency injection (DI) container.
Call the seedData.Initialize method, passing to it the database context instance.
Dispose the context when the seed method completes. The using statement
ensures the context is disposed.

The following exception occurs when Update-Database has not been run:
SqlException: Cannot open database "RazorPagesMovieContext-" requested by the

login. The login failed. Login failed for user 'user name'.

Test the app


Delete all the records in the database so the seed method will run. Stop and start the
app to seed the database. If the database isn't seeded, put a breakpoint on if
(context.Movie.Any()) and step through the code.

The app shows the seeded data:

Next steps
Previous: Scaffolded Razor Pages Next: Update the pages
Part 5, update the generated pages in
an ASP.NET Core app
Article • 06/08/2023

The scaffolded movie app has a good start, but the presentation isn't ideal. ReleaseDate
should be two words, Release Date.

Update the model


Update Models/Movie.cs with the following highlighted code:

C#

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; } = string.Empty;
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; } = string.Empty;

[Column(TypeName = "decimal(18, 2)")]


public decimal Price { get; set; }
}

In the previous code:

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.
The [Display] attribute specifies the display name of a field. In the preceding code,
Release Date instead of ReleaseDate .
The [DataType] attribute specifies the type of the data ( Date ). The time information
stored in the field isn't displayed.

DataAnnotations is covered in the next tutorial.

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.
CSHTML

@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 Anchor Tag Helper dynamically generates the HTML href
attribute value from the Razor Page (the route is relative), the asp-page , and the route
identifier ( asp-route-id ). For more information, see URL generation for Pages.

Use View Source from a browser to examine the generated markup. A portion of the
generated HTML is shown below:

HTML

<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 .
Add route template
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:

HTML

<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 returns an HTTP 404 (not found) error. For example,
https://localhost:5001/Movies/Details returns a 404 error. To make the ID optional,

append ? to the route constraint:

CSHTML

@page "{id:int?}"

Test the behavior of @page "{id:int?}" :

1. Set the page directive in Pages/Movies/Details.cshtml to @page "{id:int?}" .


2. Set a break point in public async Task<IActionResult> OnGetAsync(int? id) , in
Pages/Movies/Details.cshtml.cs .

3. Navigate to https://localhost:5001/Movies/Details/ .

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):

C#

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}
Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

if (Movie == null)
{
return NotFound();
}
return Page();
}

Review concurrency exception handling


Review the OnPostAsync method in the Pages/Movies/Edit.cshtml.cs file:

C#

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
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");
}

private bool MovieExists(int id)


{
return _context.Movie.Any(e => e.Id == id);
}

The previous code detects concurrency exceptions when one client deletes the movie
and the other client posts changes to the movie.
To test the catch block:

1. Set a breakpoint on catch (DbUpdateConcurrencyException) .


2. Select Edit for a movie, make changes, but don't enter Save.
3. In another browser window, select the Delete link for the same movie, and then
delete the movie.
4. 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:

C#

public class EditModel : PageModel


{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)


{
_context = context;
}

[BindProperty]
public Movie Movie { get; set; } = default!;

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null || _context.Movie == null)
{
return NotFound();
}

var movie = await _context.Movie.FirstOrDefaultAsync(m => m.Id ==


id);
if (movie == null)
{
return NotFound();
}
Movie = movie;
return Page();
}

// To protect from overposting attacks, enable the specific properties


you want to bind to.
// For more details, see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
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");
}

private bool MovieExists(int id)


{
return _context.Movie.Any(e => e.Id == id);
}

When an HTTP GET request is made to the Movies/Edit page, for example,
https://localhost:5001/Movies/Edit/3 :

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.

C#
[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.

Next steps
Previous: Work with a database Next: Add search
Part 6, add search to ASP.NET Core
Razor Pages
Article • 06/08/2023

By Rick Anderson

In the following sections, searching movies by genre or name is added.

Add the following highlighted code to Pages/Movies/Index.cshtml.cs :

C#

public class IndexModel : PageModel


{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)


{
_context = context;
}

public IList<Movie> Movie { get;set; } = default!;

[BindProperty(SupportsGet = true)]
public string? SearchString { get; set; }

public SelectList? Genres { get; set; }

[BindProperty(SupportsGet = true)]
public string? MovieGenre { get; set; }

In the previous code:

SearchString : Contains the text users enter in the search text box. SearchString
has the [BindProperty] attribute. [BindProperty] binds form values and query
strings with the same name as the property. [BindProperty(SupportsGet = true)]
is required for binding on HTTP 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.

2 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 into GET
binding is useful when addressing scenarios that rely on query string or route
values.

To bind a property on GET requests, set the [BindProperty] attribute's SupportsGet


property to true :

C#

[BindProperty(SupportsGet = true)]

For more information, see ASP.NET Core Community Standup: Bind on GET
discussion (YouTube) .

Update the Index page's OnGetAsync method with the following code:

C#

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

Movie = await movies.ToListAsync();


}

The first line of the OnGetAsync method creates a LINQ query to select the movies:

C#

// 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:

C#
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 . 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. 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.

7 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. SQLite with the default

collation is a mixture of case sensitive and case INsensitive, depending on the


query. For information on making case insensitive SQLite queries, see the following:

This GitHub issue


This GitHub issue
Collations and Case Sensitivity

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 .

CSHTML

@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, users cannot be expected 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 markup highlighted in the
following code:

CSHTML

@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.*@

The HTML <form> tag uses the following Tag Helpers:

Form Tag Helper. When the form is submitted, the filter string is sent to the
Pages/Movies/Index page via query string.
Input Tag Helper

Save the changes and test the filter.

Search by genre
Update the Index page's OnGetAsync method with the following code:

C#

public async Task OnGetAsync()


{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

var movies = from m in _context.Movie


select m;

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.

C#

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

The SelectList of genres is created by projecting the distinct genres.

C#

Genres = new SelectList(await genreQuery.Distinct().ToListAsync());

Add search by genre to the Razor Page


Update the Index.cshtml <form> element as highlighted in the following markup:

CSHTML

@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>

Test the app by searching by genre, by movie title, and by both.

Next steps
Previous: Update the pages Next: Add a new field
Part 7, add a new field to a Razor Page
in ASP.NET Core
Article • 06/08/2023

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 schema change to the database.

When using EF Code First to automatically create and track a database, Code First:

Adds an __EFMigrationsHistory table to the database to track whether the schema


of the database is in sync with the model classes it was generated from.
Throws an exception if the model classes aren't in sync with the database.

Automatic verification that the schema and model are in sync makes it easier to find
inconsistent database code issues.

Adding a Rating Property to the Movie Model


1. Open the Models/Movie.cs file and add a Rating property:

C#

public class Movie


{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; } = string.Empty;

[Column(TypeName = "decimal(18, 2)")]


public decimal Price { get; set; }
public string Rating { get; set; } = string.Empty;
}

2. Edit Pages/Movies/Index.cshtml , and add a Rating field:

CSHTML
@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>

3. Update the following pages with a Rating field:

Pages/Movies/Create.cshtml .
Pages/Movies/Delete.cshtml .
Pages/Movies/Details.cshtml .
Pages/Movies/Edit.cshtml .

The app won't work until the database is updated to include the new field. Running the
app without an update to the database throws a SqlException :

SqlException: Invalid column name 'Rating'.

The SqlException exception 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 developers to quickly evolve the model and database schema
together. The downside is that existing data in the database is lost. Don't use this
approach on a production database! Dropping the database 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 to keep the data. 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 make this change for each new Movie block.

C#

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M,
Rating = "R"
},

See the completed SeedData.cs file .

Build the solution.

Visual Studio

Add a migration for the rating field


1. From the Tools menu, select NuGet Package Manager > Package Manager
Console.

2. In the PMC, enter the following commands:

PowerShell

Add-Migration Rating
Update-Database

The Add-Migration command tells the framework to:

Compare the Movie model with the Movie database schema.


Create code to migrate the database schema 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.

The Update-Database command tells the framework to apply the schema changes to
the database and to preserve existing data.

Delete all the records in the database, the initializer will seed the database and
include the Rating field. Deleting can be done with the delete links in the browser
or from Sql Server Object Explorer (SSOX).

Another option is to delete the database and use migrations to re-create the
database. To delete the database in SSOX:

1. Select the database in SSOX.

2. Right-click on the database, and select Delete.

3. Check Close existing connections.

4. Select OK.

5. In the PMC, update the database:

PowerShell

Update-Database

Run the app and verify you can create, edit, and display movies with a Rating field. If
the database isn't seeded, set a break point in the SeedData.Initialize method.

Next steps
Previous: Add Search Next: Add Validation
Part 8 of tutorial series on Razor Pages
Article • 06/08/2023

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.
Rules are enforced everywhere in the app.

Add validation rules to the movie model


The System.ComponentModel.DataAnnotations namespace provides:

A set of built-in validation attributes that are applied declaratively to a class or


property.
Formatting attributes like [DataType] that help with formatting and don't provide
any validation.

Update the Movie class to take advantage of the built-in [Required] , [StringLength] ,
[RegularExpression] , and [Range] validation attributes.

C#

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie


{
public int Id { get; set; }

[StringLength(60, MinimumLength = 3)]


[Required]
public string Title { get; set; } = string.Empty;

// [Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[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; } = string.Empty;

[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; } = string.Empty;
}

The validation attributes specify behavior to enforce on the model properties they're
applied to:

The [Required] and [MinimumLength] attributes indicate that a property must have
a value. 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 spaces are allowed while
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 can set a 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.

The preceding validation rules are used for demonstration, they are not optimal for a
production system. For example, the preceding prevents entering a movie with only two
chars and doesn't allow special characters in Genre .

Having validation rules automatically enforced by ASP.NET Core helps:

Make the app more robust.


Reduce chances of saving invalid data to 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.
7 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. See this
GitHub comment 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 data annotations 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 by selecting 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:

1. Disable JavaScript in the browser. JavaScript can be disabled using browser's


developer tools. If JavaScript cannot be disabled in the browser, try another
browser.

2. Set a break point in the OnPostAsync method of the Create or Edit page.

3. Submit a form with invalid data.

4. Verify the model state is invalid:

C#

if (!ModelState.IsValid)
{
return Page();
}
Alternatively, Disable client-side validation on the server.

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.


Redisplay the form in the event of an error.

CSHTML

<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 app, validation logic is defined in one place. Validation in
one place helps keep the code clean, and makes it easier to maintain and update.

Use DataType Attributes


Examine the Movie class. The System.ComponentModel.DataAnnotations namespace
provides formatting attributes in addition to the built-in set of validation attributes. The
[DataType] attribute is applied to the ReleaseDate and Price properties.

C#

// [Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
The [DataType] attributes provide:

Hints for the view engine to format the data.


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 aren't validation attributes. In the sample
app, only the date is displayed, without time.

The DataType enumeration provides many data types, such as Date , Time , PhoneNumber ,
Currency , EmailAddress , and more.

The [DataType] attributes:

Can enable the app to automatically provide type-specific features. For example, a
mailto: link can be created for DataType.EmailAddress .

Can provide a date selector DataType.Date in browsers that support HTML5.


Emit HTML 5 data- , pronounced "data dash", attributes that HTML 5 browsers
consume.
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:

C#

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =


true)]
public DateTime ReleaseDate { get; set; }

The ApplyFormatInEditMode setting specifies that the formatting will be applied when
the value is displayed for editing. That behavior may not be wanted for some fields. For
example, in currency values, the currency symbol is usually not wanted 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. The [DataType] attribute provides the
following benefits that aren't available 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 renders data using the correct format based on its 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:

C#

[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

It's a best practice to avoid compiling hard dates in models, so using the [Range]
attribute and DateTime is discouraged. Use Configuration for date ranges and other
values that are subject to frequent change rather than specifying it in code.

The following code shows combining attributes on one line:

C#

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie


{
public int Id { get; set; }

[StringLength(60, MinimumLength = 3)]


public string Title { get; set; } = string.Empty;

[Display(Name = "Release Date"), DataType(DataType.Date)]


public DateTime ReleaseDate { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]


public string Genre { get; set; } = string.Empty;

[Range(1, 100), DataType(DataType.Currency)]


[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; } = string.Empty;
}

Get started with Razor Pages and EF Core shows advanced EF Core operations with
Razor Pages.

Apply migrations
The DataAnnotations applied to the class changes the schema. For example, the
DataAnnotations applied to the Title field:

C#

[StringLength(60, MinimumLength = 3)]


[Required]
public string Title { get; set; } = string.Empty;

Limits the characters to 60.


Doesn't allow a null value.

The Movie table currently has the following schema:

SQL

CREATE TABLE [dbo].[Movie] (


[ID] INT IDENTITY (1, 1) NOT NULL,
[Title] NVARCHAR (MAX) NULL,
[ReleaseDate] DATETIME2 (7) NOT NULL,
[Genre] NVARCHAR (MAX) NULL,
[Price] DECIMAL (18, 2) NOT NULL,
[Rating] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);

The preceding schema changes don't cause EF to throw an exception. However, create a
migration so the schema is consistent with the model.

Visual Studio

From the Tools menu, select NuGet Package Manager > Package Manager
Console. In the PMC, enter the following commands:
PowerShell

Add-Migration New_DataAnnotations
Update-Database

Update-Database runs the Up methods of the New_DataAnnotations class. Examine the


Up method:

C#

public partial class NewDataAnnotations : Migration


{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "Movie",
type: "nvarchar(60)",
maxLength: 60,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");

migrationBuilder.AlterColumn<string>(
name: "Rating",
table: "Movie",
type: "nvarchar(5)",
maxLength: 5,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");

migrationBuilder.AlterColumn<string>(
name: "Genre",
table: "Movie",
type: "nvarchar(30)",
maxLength: 30,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
}

The updated Movie table has the following schema:

SQL

CREATE TABLE [dbo].[Movie] (


[ID] INT IDENTITY (1, 1) NOT NULL,
[Title] NVARCHAR (60) NOT NULL,
[ReleaseDate] DATETIME2 (7) NOT NULL,
[Genre] NVARCHAR (30) NOT NULL,
[Price] DECIMAL (18, 2) NOT NULL,
[Rating] NVARCHAR (5) NOT NULL,
CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);

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

Next steps
Previous: Add a new field
Get started with ASP.NET Core MVC
Article • 05/02/2023

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. See Choose an ASP.NET Core UI, which
compares Razor Pages, MVC, and Blazor for UI development.

This is the first tutorial of a series that teaches ASP.NET Core MVC web development
with controllers and views.

At the end of the series, you'll have an app that manages and displays movie data. You
learn how to:

" Create a web app.


" Add and scaffold a model.
" Work with a database.
" Add search and validation.

View or download sample code (how to download).

Prerequisites
Visual Studio

Visual Studio 2022 with the ASP.NET and web development workload.
Create a web app
Visual Studio

Start Visual Studio and select Create a new project.


In the Create a new project dialog, select ASP.NET Core Web App (Model-
View-Controller) > Next.
In the Configure your new project dialog, enter MvcMovie for Project name.
It's important to name the project MvcMovie. Capitalization needs to match
each namespace when code is copied.
Select Next.
In the Additional information dialog:
Select .NET 7.0.
Verify that Do not use top-level statements is unchecked.
Select Create.
For more information, including alternative approaches to create the project, see
Create a new project in Visual Studio.

Visual Studio uses the default project template for the created MVC project. The
created project:

Is a working app.
Is a basic starter project.

Run the app

Visual Studio

Select Ctrl+F5 to run the app without the debugger.

Visual Studio displays the following dialog when a project is not yet
configured to use SSL:
Select Yes if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select Yes if you agree to trust the development certificate.

For information on trusting the Firefox browser, see Firefox


SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

Visual Studio runs the app and opens the default browser.

The address bar shows localhost:<port#> and not something like example.com . The
standard hostname for your local computer is localhost . When Visual Studio
creates a web project, a random port is used for the web server.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

Make code changes.


Save the file.
Quickly refresh the browser and see the code changes.

You can launch the app in debug or non-debug mode from the Debug menu:

You can debug the app by selecting the https button in the toolbar:

The following image shows the app:


Visual Studio

Visual Studio help


Learn to debug C# code using Visual Studio
Introduction to the Visual Studio IDE

In the next tutorial in this series, you learn about MVC and start writing some code.

Next: Add a controller


Part 2, add a controller to an ASP.NET
Core MVC app
Article • 05/02/2023

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.
Retrieve model data.
Call view templates that return a response.

In an MVC app, the view only displays information. The controller handles and responds
to user input and interaction. For example, the controller handles URL segments and
query-string values, and passes these values to the model. The model might use these
values to query the database. For example:

https://localhost:5001/Home/Privacy : specifies the Home controller and the


Privacy action.

https://localhost:5001/Movies/Edit/5 : is a request to edit the movie with ID=5


using the Movies controller and the Edit action, which are detailed later in the
tutorial.

Route data is explained later in the tutorial.

The MVC architectural pattern separates an app into three main groups of components:
Models, Views, and Controllers. This pattern helps to achieve separation of concerns:
The UI logic belongs in the view. Input logic belongs in the controller. Business logic
belongs in the model. This separation helps manage complexity when building an app,
because it enables work on one aspect of the implementation at a time without
impacting the code of another. For example, you can work on the view code without
depending on the business logic code.

These concepts are introduced and demonstrated in this tutorial series while building a
movie app. The MVC project contains folders for the Controllers and Views.

Add a controller
Visual Studio

In Solution Explorer, right-click Controllers > Add > Controller.

In the Add New Scaffolded Item dialog box, select MVC Controller - Empty > Add.
In the Add New Item - MvcMovie dialog, enter HelloWorldController.cs and select
Add.

Replace the contents of Controllers/HelloWorldController.cs with the following code:

C#

using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller


{
//
// GET: /HelloWorld/
public string Index()
{
return "This is my default action...";
}
//
// GET: /HelloWorld/Welcome/
public string Welcome()
{
return "This is the Welcome action method...";
}
}
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 .

Combines:
The protocol used: HTTPS .
The network location of the web server, including the TCP port: localhost:5001 .
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 without the debugger.

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]

The routing format is set in the Program.cs file.

C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

When you browse to the app and don't supply any URL segments, it defaults to the
"Home" controller and the "Index" method specified in the template line highlighted
above. In the preceding URL segments:

The first URL segment determines the controller class to run. So


localhost:5001/HelloWorld maps to the HelloWorld Controller class.

The second part of the URL segment determines the action method on the class.
So localhost:5001/HelloWorld/Index causes the Index method of the
HelloWorldController class to run. Notice that you only had to browse to

localhost:5001/HelloWorld and the Index method was called by default. 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 . Replace {PORT} with your


port number.

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.

C#

// 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}");
}

The preceding code:

Uses the C# optional-parameter feature to indicate that the numTimes parameter


defaults to 1 if no value is passed for that parameter.
Uses HtmlEncoder.Default.Encode to protect the app from malicious input, such as
through JavaScript.
Uses Interpolated Strings in $"Hello {name}, NumTimes is: {numTimes}" .

Run the app and browse to: https://localhost:{PORT}/HelloWorld/Welcome?


name=Rick&numtimes=4 . Replace {PORT} with your port number.

Try different values for name and numtimes in the URL. The MVC model binding system
automatically maps the named parameters from the query string to parameters in the
method. See Model Binding for more information.

In the previous image:

The URL segment Parameters isn't used.


The name and numTimes parameters are passed in the query string .
The ? (question mark) in the above URL is a separator, and the query string
follows.
The & character separates field-value pairs.

Replace the Welcome method with the following code:

C#

public string Welcome(string name, int ID = 1)


{
return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}

Run the app and enter the following URL: https://localhost:


{PORT}/HelloWorld/Welcome/3?name=Rick

In the preceding URL:

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 ? starts the query string .

C#

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

In the preceding example:

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.

Previous: Get Started Next: Add a View


Part 3, add a view to an ASP.NET Core
MVC app
Article • 05/02/2023

By Rick Anderson

In this section, you modify the HelloWorldController class to use Razor view files. This
cleanly encapsulates the process of generating HTML responses to a client.

View templates are created using Razor. Razor-based view templates:

Have a .cshtml file extension.


Provide an elegant way to create HTML output with C#.

Currently the Index method returns a string with a message in the controller class. In
the HelloWorldController class, replace the Index method with the following code:

C#

public IActionResult Index()


{
return View();
}

The preceding code:

Calls the controller's View method.


Uses a view template to generate an HTML response.

Controller methods:

Are referred to as action methods. For example, the Index action method in the
preceding code.
Generally return an IActionResult or a class derived from ActionResult, not a type
like string .

Add a view
Visual Studio

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 - Empty
Keep the Name box value, Index.cshtml .
Select Add

Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the
following:

CSHTML

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>

Navigate to https://localhost:{PORT}/HelloWorld :

The Index method in the HelloWorldController ran the statement return View(); ,
which specified that the method should use a view template file to render a
response to the browser.

A view template file name wasn't specified, so MVC defaulted to using the default
view file. When the view file name isn't specified, the default view is returned. The
default view has the same name as the action method, Index in this example. The
view template /Views/HelloWorld/Index.cshtml is used.

The following image shows the string "Hello from our View Template!" hard-coded
in the view:

Change views and layout pages


Select the menu links MvcMovie, Home, and Privacy. Each page shows the same menu
layout. The menu layout is implemented in the Views/Shared/_Layout.cshtml file.

Open the Views/Shared/_Layout.cshtml file.

Layout templates allow:

Specifying the HTML container layout of a site in one place.


Applying the HTML container layout across multiple pages in the site.

Find the @RenderBody() line. RenderBody is a placeholder where all the view-specific
pages you create show up, wrapped in the layout page. For example, if you select the
Privacy link, the Views/Home/Privacy.cshtml view is rendered inside the RenderBody
method.

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:

CSHTML

<!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" asp-append-version="true"
/>
</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-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Movies"
asp-action="Index">Movie App</a>
<button class="navbar-toggler" type="button" data-bs-
toggle="collapse" data-bs-target=".navbar-collapse" aria-
controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle
navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">


<div class="container">
&copy; 2022 - Movie App - <a asp-area="" asp-controller="Home"
asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

The preceding markup made the following changes:

Three occurrences of MvcMovie to Movie App .


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> .

In the preceding markup, the asp-area="" anchor Tag Helper attribute and attribute
value was omitted because this app isn't using Areas.

Note: The Movies controller hasn't been implemented. At this point, the Movie App link
isn't functional.

Save the changes and select the Privacy link. Notice how the title on the browser tab
displays Privacy Policy - Movie App instead of Privacy Policy - MvcMovie

Select the Home link.

Notice that the title and anchor text display Movie App. The changes were made once
in the layout template and all pages on the site reflect the new link text and new title.

Examine the Views/_ViewStart.cshtml file:


CSHTML

@{
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.

Open the Views/HelloWorld/Index.cshtml view file.

Change the title and <h2> element as highlighted in the following:

CSHTML

@{
ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>

The title and <h2> element are slightly different so it's clear which part of the 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:

CSHTML

<title>@ViewData["Title"] - Movie App</title>

Save the change and navigate to https://localhost:{PORT}/HelloWorld .

Notice that the following have changed:

Browser title.
Primary heading.
Secondary headings.

If there are no changes in the browser, it could be cached content that is being viewed.
Press Ctrl+F5 in the browser to force the response from the server to be loaded. The
browser title is created with ViewData["Title"] we set in the Index.cshtml view
template and the additional "- Movie App" added in the layout file.

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.

The small bit of "data", the "Hello from our View Template!" message, is hard-coded
however. The MVC application has a "V" (view), a "C" (controller), but no "M" (model)
yet.

Passing Data from the Controller to the View


Controller actions are invoked in response to an incoming URL request. A controller
class is where the code is written that handles the incoming browser requests. The
controller retrieves data from a data source and decides what type of response to send
back to the browser. View templates can be used from a controller to generate and
format an HTML response to the browser.

Controllers are responsible for providing the data required in order for a view template
to render a response.

View templates should not:

Do business logic
Interact with a database directly.

A view template should work only with the data that's provided to it by the controller.
Maintaining this "separation of concerns" helps keep the code:
Clean.
Testable.
Maintainable.

Currently, the Welcome method in the HelloWorldController class takes a name and an
ID parameter and then outputs the values directly to the browser.

Rather than have the controller render this response as a string, change the controller to
use a view template instead. The view template generates a dynamic response, which
means that appropriate data must be passed from the controller to the view to generate
the response. Do this by having the controller put the dynamic data (parameters) that
the view template needs in a ViewData dictionary. The view template can then access
the dynamic data.

In HelloWorldController.cs , change the Welcome method to add a Message and


NumTimes value to the ViewData dictionary.

The ViewData dictionary is a dynamic object, which means any type can be used. The
ViewData object has no defined properties until something is added. The MVC model

binding system automatically maps the named parameters name and numTimes from the
query string to parameters in the method. The complete HelloWorldController :

C#

using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller


{
public IActionResult Index()
{
return View();
}
public IActionResult Welcome(string name, int numTimes = 1)
{
ViewData["Message"] = "Hello " + name;
ViewData["NumTimes"] = numTimes;
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:

CSHTML

@{
ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>

Save your changes and browse to the following URL:

https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4

Data is taken from the URL and passed to the controller using the MVC model binder.
The controller packages the data into a ViewData dictionary and passes that object to
the view. The view then renders the data as HTML to the browser.

In the preceding sample, 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 preferred over the
ViewData dictionary approach.

In the next tutorial, a database of movies is created.

Previous: Add a Controller Next: Add a Model


Part 4, add a model to an ASP.NET Core
MVC app
Article • 05/02/2023

By Rick Anderson and Jon P Smith .

In this tutorial, classes are added for managing movies in a database. These classes are
the "Model" part of the MVC app.

These model classes are used with Entity Framework Core (EF Core) to work with a
database. EF Core is an object-relational mapping (ORM) framework that simplifies the
data access code that you have to write.

The model classes created are known as POCO classes, from Plain Old CLR Objects.
POCO classes don't have any dependency on EF Core. They only define the properties of
the data to be stored in the database.

In this tutorial, model classes are created first, and EF Core creates the database.

Add a data model class


Visual Studio

Right-click the Models folder > Add > Class. Name the file Movie.cs .

Update the Models/Movie.cs file with the following code:

C#

using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models;

public class Movie


{
public int Id { get; set; }
public string? Title { get; set; }
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string? Genre { get; set; }
public decimal Price { get; set; }
}
The Movie class contains an Id field, which is required by the database for the primary
key.

The DataType attribute on ReleaseDate specifies the type of the data ( Date ). With this
attribute:

The user isn't required to enter time information in the date field.
Only the date is displayed, not time information.

DataAnnotations are covered in a later tutorial.

The question mark after string indicates that the property is nullable. For more
information, see Nullable reference types.

Add NuGet packages


Visual Studio

Visual Studio automatically installs the required packages.

Build the project as a check for compiler errors.

Scaffold movie pages


Use the scaffolding tool to produce Create , Read , Update , and Delete (CRUD) pages for
the movie model.

Visual Studio

In Solution Explorer, right-click the Controllers folder and select Add > New
Scaffolded Item.
In the Add New Scaffolded Item dialog:

In the left pane, select Installed > Common > MVC.


Select MVC Controller with views, using Entity Framework.
Select Add.
Complete the Add MVC Controller with views, using Entity Framework dialog:

In the Model class drop down, select Movie (MvcMovie.Models).


In the Data context class row, select the + (plus) sign.
In the Add Data Context dialog, the class name
MvcMovie.Data.MvcMovieContext is generated.
Select Add.
In the Database provider drop down, select SQL Server.
Views and Controller name: Keep the default.
Select Add.
If you get an error message, select Add a second time to try it again.

Scaffolding adds the following packages:

Microsoft.EntityFrameworkCore.SqlServer

Microsoft.EntityFrameworkCore.Tools

Microsoft.VisualStudio.Web.CodeGeneration.Design

Scaffolding creates the following:

A movies controller: Controllers/MoviesController.cs


Razor view files for Create, Delete, Details, Edit, and Index pages:
Views/Movies/*.cshtml

A database context class: Data/MvcMovieContext.cs

Scaffolding updates the following:

Inserts required package references in the MvcMovie.csproj project file.


Registers the database context in the Program.cs file.
Adds a database connection string to the appsettings.json file.

The automatic creation of these files and file updates is known as scaffolding.

The scaffolded pages can't be used yet because the database doesn't exist. Running
the app and selecting the Movie App link results in a Cannot open database or no
such table: Movie error message.
Build the app to verify that there are no errors.

Initial migration
Use the EF Core Migrations feature to create the database. Migrations is a set of tools
that create and update a database to match the data model.

Visual Studio

From the Tools menu, select NuGet Package Manager > Package Manager
Console .

In the Package Manager Console (PMC), enter the following commands:

PowerShell

Add-Migration InitialCreate
Update-Database

Add-Migration InitialCreate : Generates a


Migrations/{timestamp}_InitialCreate.cs migration file. The InitialCreate

argument is the migration name. Any name can be used, but by convention, a
name is selected that describes the migration. Because this is the first
migration, the generated class contains code to create the database schema.
The database schema is based on the model specified in the MvcMovieContext
class.

Update-Database : Updates the database to the latest migration, which the


previous command created. This command runs the Up method in the
Migrations/{time-stamp}_InitialCreate.cs file, which creates the database.

The Update-Database command generates 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()'.

Ignore the preceding warning, it's fixed in a later tutorial.


For more information on the PMC tools for EF Core, see EF Core tools reference -
PMC in Visual Studio.

Test the app


Visual Studio

Run the app and select the Movie App link.

If you get an exception similar to the following, you may have missed the Update-
Database command in the migrations step:

Console

SqlException: Cannot open database "MvcMovieContext-1" requested by the


login. The login failed.

7 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 .

Examine the generated database context class and


registration
With EF Core, data access is performed using a model. A model is made up of entity
classes and a context object that represents a session with the database. The context
object allows querying and saving data. The database context is derived from
Microsoft.EntityFrameworkCore.DbContext and specifies the entities to include in the
data model.

Scaffolding creates the Data/MvcMovieContext.cs database context class:

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
public class MvcMovieContext : DbContext
{
public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
: base(options)
{
}

public DbSet<MvcMovie.Models.Movie> Movie { get; set; }


}
}

The preceding code creates a DbSet<Movie> property that represents the movies in the
database.

Dependency injection
ASP.NET Core is built with dependency injection (DI). Services, such as the database
context, are registered with DI in Program.cs . These services are provided to
components that require them via constructor parameters.

In the Controllers/MoviesController.cs file, the constructor uses Dependency Injection


to inject the MvcMovieContext database context into the controller. The database context
is used in each of the CRUD methods in the controller.

Scaffolding generated the following highlighted code in Program.cs :

Visual Studio

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));

The ASP.NET Core configuration system reads the "MvcMovieContext" database


connection string.
Examine the generated database connection string
Scaffolding added a connection string to the appsettings.json file:

Visual Studio

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MvcMovieContext": "Server=
(localdb)\\mssqllocaldb;Database=MvcMovieContext-
7dc5;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

For local development, the ASP.NET Core configuration system reads the
ConnectionString key from the appsettings.json file.

The InitialCreate class


Examine the Migrations/{timestamp}_InitialCreate.cs migration file:

C#

using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Movie",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(type: "nvarchar(max)",
nullable: true),
ReleaseDate = table.Column<DateTime>(type: "datetime2",
nullable: false),
Genre = table.Column<string>(type: "nvarchar(max)",
nullable: true),
Price = table.Column<decimal>(type: "decimal(18,2)",
nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Movie", x => x.Id);
});
}

protected override void Down(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropTable(
name: "Movie");
}
}
}

In the preceding code:

InitialCreate.Up creates the Movie table and configures Id as the primary key.

InitialCreate.Down reverts the schema changes made by the Up migration.

Dependency injection in the controller


Open the Controllers/MoviesController.cs file and examine the constructor:

C#

public class MoviesController : Controller


{
private readonly MvcMovieContext _context;

public MoviesController(MvcMovieContext context)


{
_context = context;
}

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.
Test the Create page. Enter and submit data.

Test the Edit, Details, and Delete pages.

Strongly typed models and the @model directive


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 provides the ability to pass strongly typed model objects to a view. This strongly
typed approach enables compile time code checking. The scaffolding mechanism
passed a strongly typed model in the MoviesController class and views.

Examine the generated Details method in the Controllers/MoviesController.cs file:

C#

// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie


.FirstOrDefaultAsync(m => m.Id == id);
if (movie == 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.

The id can be passed in with a query string, as in the following example:

https://localhost:5001/movies/details?id=1
The id parameter is defined as a nullable type ( int? ) in cases when the id value isn't
provided.

A lambda expression is passed in to the FirstOrDefaultAsync method to select movie


entities that match the route data or query string value.

C#

var movie = await _context.Movie


.FirstOrDefaultAsync(m => m.Id == id);

If a movie is found, an instance of the Movie model is passed to the Details view:

C#

return View(movie);

Examine the contents of the Views/Movies/Details.cshtml file:

CSHTML

@model MvcMovie.Models.Movie

@{
ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
<h4>Movie</h4>
<hr />
<dl class="row">
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Title)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.ReleaseDate)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.ReleaseDate)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Genre)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Genre)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Price)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>

The @model statement at the top of the view file specifies the type of object that the
view expects. When the movie controller was created, the following @model statement
was included:

CSHTML

@model MvcMovie.Models.Movie

This @model directive allows access to the movie that the controller passed to the view.
The Model object is strongly typed. For example, in the Details.cshtml view, the code
passes each movie field to the DisplayNameFor and DisplayFor HTML Helpers with the
strongly typed Model object. The Create and Edit methods and views also pass a
Movie model object.

Examine the Index.cshtml view and the Index method in the Movies controller. Notice
how the code creates a List object when it calls the View method. The code passes this
Movies list from the Index action method to the view:

C#

// GET: Movies
public async Task<IActionResult> Index(string searchString)
{
return _context.Movie != null ?
View(await _context.Movie.ToListAsync()) :
Problem("Entity set 'MvcMovieContext.Movie' is null.");
}

The code returns problem details if the Movie property of the data context is null.

When the movies controller was created, scaffolding included the following @model
statement at the top of the Index.cshtml file:
CSHTML

@model IEnumerable<MvcMovie.Models.Movie>

The @model directive allows access to the list of movies that the controller passed to the
view by using a Model object that's strongly typed. For example, in the Index.cshtml
view, the code loops through the movies with a foreach statement over the strongly
typed Model object:

CSHTML

@model IEnumerable<MvcMovie.Models.Movie>

@{
ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a>
|
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Because the Model object is strongly typed as an IEnumerable<Movie> object, each item
in the loop is typed as Movie . Among other benefits, the compiler validates the types
used in the code.

Additional resources
Entity Framework Core for Beginners
Tag Helpers
Globalization and localization

Previous: Adding a View Next: Working with SQL


Part 5, work with a database in an
ASP.NET Core MVC app
Article • 05/02/2023

By Rick Anderson and Jon P Smith .

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 Program.cs file:

Visual Studio

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));

The ASP.NET Core Configuration system reads the ConnectionString key. For local
development, it gets the connection string from the appsettings.json file:

JSON

"ConnectionStrings": {
"MvcMovieContext": "Server=
(localdb)\\mssqllocaldb;Database=MvcMovieContext-
7dc5;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 production SQL Server. For more information,
see Configuration.

Visual Studio

SQL Server Express LocalDB


LocalDB:

Is a lightweight version of the SQL Server Express Database Engine, installed


by default with Visual Studio.
Starts on demand by using a connection string.
Is targeted for program development. It runs in user mode, so there's no
complex configuration.
By default creates .mdf files in the C:/Users/{user} directory.

Examine the database


From the View menu, open SQL Server Object Explorer (SSOX).

Right-click on the Movie table ( dbo.Movie ) > View Designer


Note the key icon next to ID . By default, EF makes a property named ID the
primary key.

Right-click on the Movie table > View Data


Seed the database
Create a new class named SeedData in the Models folder. Replace the generated code
with the following:

C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;

namespace MvcMovie.Models;

public static class SeedData


{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<
DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}

If there are any movies in the database, the seed initializer returns and no movies are
added.

C#

if (context.Movie.Any())
{
return; // DB has been seeded.
}

Add the seed initializer

Visual Studio

Replace the contents of Program.cs with the following code. The new code is
highlighted.

C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));

// Add services to the container.


builder.Services.AddControllersWithViews();

var app = builder.Build();


using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;

SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/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.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Delete all the records in the database. You can do this with the delete links in the
browser or from SSOX.

Test the app. Force the app to initialize, calling the code in the Program.cs file, so
the seed method runs. To force initialization, close the command prompt window
that Visual Studio opened, and restart by pressing Ctrl+F5.

The app shows the seeded data.


Previous: Adding a model Next: Adding controller methods and views
Part 6, controller methods and views in
ASP.NET Core
Article • 05/02/2023

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:

C#

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; }
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string? Genre { get; set; }
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
}

DataAnnotations are explained 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.
CSHTML

<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:

HTML

<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>

Recall the format for routing set in the Program.cs file:

C#

app.MapControllerRoute(
name: "default",
pattern: "{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.

C#

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.FindAsync(id);


if (movie == null)
{
return NotFound();
}
return View(movie);
}

The following code shows the HTTP POST Edit method, which processes the posted
movie values:

C#

// POST: Movies/Edit/5
// To protect from overposting attacks, 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,Rating")] Movie movie)
{
if (id != movie.Id)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}

The [Bind] attribute is one way to protect against over-posting. You should only include
properties in the [Bind] attribute that you want to change. For more information, see
Protect your controller from over-posting. ViewModels provide an alternative
approach to prevent over-posting.

Notice the second Edit action method is preceded by the [HttpPost] attribute.

C#

// POST: Movies/Edit/5
// To protect from overposting attacks, 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,Rating")] Movie movie)
{
if (id != movie.Id)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
The 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.

CSHTML

<form asp-action="Edit">

The Form Tag Helper generates a hidden anti-forgery token that must match the
[ValidateAntiForgeryToken] generated anti-forgery token in the Edit method of the
Movies controller. For more information, see Prevent Cross-Site Request Forgery
(XSRF/CSRF) attacks in ASP.NET Core.

The HttpGet Edit method takes the movie ID parameter, looks up the movie using the
Entity Framework FindAsync method, and returns the selected movie to the Edit view. If
a movie cannot be found, NotFound (HTTP 404) is returned.

C#

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.FindAsync(id);


if (movie == null)
{
return NotFound();
}
return View(movie);
}

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:
CSHTML

@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.

HTML

<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="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmq
UyXnJBXhmrjcUVDJyDUMm7-
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.

Processing the POST Request


The following listing shows the [HttpPost] version of the Edit action method.

C#

// POST: Movies/Edit/5
// To protect from overposting attacks, 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,Rating")] Movie movie)
{
if (id != movie.Id)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}

The [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 property verifies that the
data submitted in the form can be used to modify (edit or update) a Movie object. If the
data is valid, it's saved. The updated (edited) movie data is saved to the database by
calling the SaveChangesAsync method of database context. After saving the data, the
code redirects the user to the Index action method of the MoviesController class, which
displays the movie collection, including the changes just made.

Before the form is posted to the server, client-side validation checks any validation rules
on the fields. If there are any validation errors, an error message is displayed and the
form isn't posted. If JavaScript is disabled, you won't have client-side validation but the
server will detect the posted values that are not valid, and the form values will be
redisplayed with error messages. Later in the tutorial we examine Model Validation in
more detail. The Validation Tag Helper in the Views/Movies/Edit.cshtml view template
takes care of displaying appropriate error messages.
All the HttpGet methods in the movie controller follow a similar pattern. They get a
movie object (or list of objects, in the case of Index ), and pass the object (model) to the
view. The Create method passes an empty movie object to the Create view. All the
methods that create, edit, delete, or otherwise modify data do so in the [HttpPost]
overload of the method. Modifying data in an HTTP GET method is a security risk.
Modifying data in an HTTP GET method also violates HTTP best practices and the
architectural REST pattern, which specifies that GET requests shouldn't change the
state of your application. In other words, performing a GET operation should be a safe
operation that has no side effects and doesn't modify your persisted data.
Additional resources
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core
Protect your controller from over-posting
ViewModels
Form Tag Helper
Input Tag Helper
Label Tag Helper
Select Tag Helper
Validation Tag Helper

Previous Next
Part 7, add search to an ASP.NET Core
MVC app
Article • 05/02/2023

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:

C#

public async Task<IActionResult> Index(string searchString)


{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}

return View(await movies.ToListAsync());


}

The following line in the Index action method creates a LINQ query to select the
movies:

C#

var movies = from m in _context.Movie


select m;

The query is only defined at this point, it has not been run against the database.

If the searchString parameter contains a string, the movies query is modified to filter
on the value of the search string:
C#

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}

The s => s.Title!.Contains(searchString) 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
Program.cs .

C#

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

Change the parameter to id and change all occurrences of searchString to id .

The previous Index method:

C#

public async Task<IActionResult> Index(string searchString)


{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}

return View(await movies.ToListAsync());


}

The updated Index method with id parameter:

C#

public async Task<IActionResult> Index(string id)


{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title!.Contains(id));
}
return View(await movies.ToListAsync());
}

You can now pass the search title as route data (a URL segment) instead of as a query
string value.

However, you can't expect users to modify the URL every time they want to search for a
movie. So now you'll add UI elements to help them filter movies. If you changed the
signature of the Index method to test how to pass the route-bound ID parameter,
change it back so that it takes a parameter named searchString :

C#

public async Task<IActionResult> Index(string searchString)


{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}

var movies = from m in _context.Movie


select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
return View(await movies.ToListAsync());
}

Open the Views/Movies/Index.cshtml file, and add the <form> markup highlighted
below:

CSHTML

@model IEnumerable<MvcMovie.Models.Movie>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">


<p>
Title: <input type="text" name="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>

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.

C#

[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.
CSHTML

@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">

Now when you submit a search, the URL contains the search query string. Searching will
also go to the HttpGet Index action method, even if you have a HttpPost Index
method.

The following markup shows the change to the form tag:

CSHTML
<form asp-controller="Movies" asp-action="Index" method="get">

Add Search by genre


Add the following MovieGenreViewModel class to the Models folder:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel


{
public List<Movie>? Movies { get; set; }
public SelectList? Genres { get; set; }
public string? MovieGenre { get; set; }
public string? SearchString { get; set; }
}

The movie-genre view model will contain:

A list of movies.
A SelectList containing the list of genres. This allows the user to select a genre
from the list.
MovieGenre , which contains the selected genre.
SearchString , which contains the text users enter in the search text box.

Replace the Index method in MoviesController.cs with the following code:

C#

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string
searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;

if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}

if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}

var movieGenreVM = new MovieGenreViewModel


{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};

return View(movieGenreVM);
}

The following code is a LINQ query that retrieves all the genres from the database.

C#

// Use LINQ to get list of genres.


IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

The SelectList of genres is created by projecting the distinct genres (we don't want our
select list to have duplicate genres).

When the user searches for the item, the search value is retained in the search box.

Add search by genre to the Index view


Update Index.cshtml found in Views/Movies/ as follows:

CSHTML

@model MvcMovie.Models.MovieGenreViewModel

@{
ViewData["Title"] = "Index";
}

<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>

<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.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. The ! after model.Movies is the null-forgiving
operator, which is used to declare that Movies isn't null.

Test the app by searching by genre, by movie title, and by both:

Previous Next
Part 8, add a new field to an ASP.NET
Core MVC app
Article • 05/02/2023

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.

Add a Rating Property to the Movie Model


Add a Rating property to Models/Movie.cs :

C#

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

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string? Genre { get; set; }

[Column(TypeName = "decimal(18, 2)")]


public decimal Price { get; set; }
public string? Rating { get; set; }
}
Build the app

Visual Studio

Ctrl+Shift+B

Because you've added a new field to the Movie class, you need to update the property
binding list so this new property will be included. In MoviesController.cs , update the
[Bind] attribute for both the Create and Edit action methods to include the Rating

property:

C#

[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:

CSHTML

<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>
@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> |
<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>

Update the /Views/Movies/Create.cshtml with a Rating field.

Visual Studio / Visual Studio for Mac

You can copy/paste the previous "form group" and let intelliSense help you update
the fields. IntelliSense works with Tag Helpers.
Update the remaining templates.

Update the SeedData class so that it provides a value for the new column. A sample
change is shown below, but you'll want to make this change for each new Movie .

C#

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

From the Tools menu, select NuGet Package Manager > Package Manager
Console.

In the PMC, enter the following commands:

PowerShell

Add-Migration Rating
Update-Database

The Add-Migration command tells the migration framework to examine the current
Movie model with the current 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, and display movies with a Rating field.

Previous Next
Part 9, add validation to an ASP.NET
Core MVC app
Article • 05/02/2023

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.

Keeping things DRY


One of the design tenets of MVC is DRY ("Don't Repeat Yourself"). ASP.NET Core MVC
encourages you to specify functionality or behavior only once, and then have it be
reflected everywhere in an app. This reduces the amount of code you need to write and
makes the code you do write less error prone, easier to test, and easier to maintain.

The validation support provided by MVC and Entity Framework Core Code First is a
good example of the DRY principle in action. You can declaratively specify validation
rules in one place (in the model class) and the rules are enforced everywhere in the app.

Add validation rules to the movie model


The DataAnnotations namespace provides a set of built-in validation attributes that are
applied declaratively to a class or property. DataAnnotations also contains formatting
attributes like DataType that help with formatting and don't provide any validation.

Update the Movie class to take advantage of the built-in validation attributes Required ,
StringLength , RegularExpression , Range and the DataType formatting attribute.

C#

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie


{
public int Id { get; set; }

[StringLength(60, MinimumLength = 3)]


[Required]
public string? Title { get; set; }

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[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 spaces are allowed while
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.

Select 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.
7 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. See this
GitHub comment 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.

How validation works


You might wonder how the validation UI was generated without any updates to the
code in the controller or views. The following code shows the two Create methods.

C#

// GET: Movies/Create
public IActionResult Create()
{
return View();
}

// POST: Movies/Create
// To protect from overposting attacks, 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>
Create([Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(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.
A portion of the Create.cshtml view template is shown in the following markup:

HTML

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

@*Markup removed for brevity.*@

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.

Using DataType Attributes


Open the Movie.cs file and examine the Movie class. The
System.ComponentModel.DataAnnotations namespace provides formatting attributes in

addition to the built-in set of validation attributes. We've already applied a DataType
enumeration value to the release date and to the price fields. The following code shows
the ReleaseDate and Price properties with the appropriate DataType attribute.

C#

[Display(Name = "Release Date")]


[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
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:

C#

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =


true)]
public DateTime ReleaseDate { get; set; }

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).

7 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:

C#

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie


{
public int Id { get; set; }
[StringLength(60, MinimumLength = 3)]
public string Title { get; set; }
[Display(Name = "Release Date"), DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
public string Genre { get; set; }
[Range(1, 100), DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { 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

Previous Next
Part 10, examine the Details and Delete
methods of an ASP.NET Core app
Article • 05/02/2023

By Rick Anderson

Open the Movie controller and examine the Details method:

C#

// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie


.FirstOrDefaultAsync(m => m.Id == id);
if (movie == 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 Program.cs .

C#

app.MapControllerRoute(
name: "default",
pattern: "{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.

C#

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{

if (id == null)
{
return NotFound();
}

var movie = await _context.Movie


.FirstOrDefaultAsync(m => m.Id == id);
if (movie == null)
{
return NotFound();
}

return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}

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:
C#

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{

C#

// 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:

C#

// 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 an ASP.NET Core and SQL
Database app in Azure App Service.

Previous
ASP.NET Core Blazor tutorials
Article • 05/30/2023

The following tutorials provide basic working experiences for building Blazor apps.

For an overview of Blazor, see ASP.NET Core Blazor. For more information on the Blazor
hosting models, Blazor Server and Blazor WebAssembly, see ASP.NET Core Blazor
hosting models.

Build your first Blazor app (Blazor Server)

Build a Blazor todo list app (Blazor Server or Blazor WebAssembly)

Use ASP.NET Core SignalR with Blazor (Blazor Server or Blazor WebAssembly)

ASP.NET Core Blazor Hybrid tutorials

Learn modules
Tutorial: Create a web API with ASP.NET
Core
Article • 05/17/2023

By Rick Anderson and Kirk Larkin

This tutorial teaches the basics of building a controller-based web API that uses a
database. Another approach to creating APIs in ASP.NET Core is to create minimal APIs.
For help in choosing between minimal APIs and controller-based APIs, see APIs
overview. For a tutorial on creating a minimal API, see Tutorial: Create a minimal API
with ASP.NET Core.

Overview
This tutorial creates the following API:

API Description Request Response body


body

GET /api/todoitems Get all to-do items None Array of to-do items

GET /api/todoitems/{id} Get an item by ID None To-do item

POST /api/todoitems Add a new item To-do item To-do item

PUT /api/todoitems/{id} Update an existing item To-do item None

DELETE /api/todoitems/{id} Delete an item None None

The following diagram shows the design of the app.


Prerequisites
Visual Studio

Visual Studio 2022 with the ASP.NET and web development workload.

Create a web project


Visual Studio
From the File menu, select New > Project.
Enter Web API in the search box.
Select the ASP.NET Core Web API template and select Next.
In the Configure your new project dialog, name the project TodoApi and
select Next.
In the Additional information dialog:
Confirm the Framework is .NET 7.0 (or later).
Confirm the checkbox for Use controllers(uncheck to use minimal APIs) is
checked.
Select Create.

7 Note

For guidance on adding packages to .NET apps, see the articles under Install and
manage packages at Package consumption workflow (NuGet documentation).
Confirm correct package versions at NuGet.org .

Test the project


The project template creates a WeatherForecast API with support for Swagger.

Visual Studio

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to
use SSL:

Select Yes if you trust the IIS Express SSL certificate.


The following dialog is displayed:

Select Yes if you agree to trust the development certificate.

For information on trusting the Firefox browser, see Firefox


SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

Visual Studio launches the default browser and navigates to https://localhost:


<port>/swagger/index.html , where <port> is a randomly chosen port number.

The Swagger page /swagger/index.html is displayed. Select GET > Try it out > Execute.
The page displays:

The Curl command to test the WeatherForecast API.


The URL to test the WeatherForecast API.
The response code, body, and headers.
A drop-down list box with media types and the example value and schema.

If the Swagger page doesn't appear, see this GitHub issue .

Swagger is used to generate useful documentation and help pages for web APIs. This
tutorial focuses on creating a web API. For more information on Swagger, see ASP.NET
Core web API documentation with Swagger / OpenAPI.

Copy and paste the Request URL in the browser: https://localhost:


<port>/weatherforecast
JSON similar to the following example is returned:

JSON

[
{
"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"
}
]

Add a model class


A model is a set of classes that represent the data that the app manages. The model for
this app is the TodoItem class.

Visual Studio

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:

C#

namespace TodoApi.Models;

public class TodoItem


{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}

The Id property functions as the unique key in a relational database.

Model classes can go anywhere in the project, but the Models folder is used by
convention.

Add a database context


The database context is the main class that coordinates Entity Framework functionality
for a data model. This class is created by deriving from the
Microsoft.EntityFrameworkCore.DbContext class.

Visual Studio

Add NuGet packages


From the Tools menu, select NuGet Package Manager > Manage NuGet
Packages for Solution.
Select the Browse tab, and then enter
Microsoft.EntityFrameworkCore.InMemory in the search box.
Select Microsoft.EntityFrameworkCore.InMemory in the left pane.
Select the Project checkbox in the right pane and then select Install.

Add the TodoContext database context


Right-click the Models folder and select Add > Class. Name the class
TodoContext and click Add.
Enter the following code:

C#

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models;

public class TodoContext : DbContext


{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}

public DbSet<TodoItem> TodoItems { get; set; } = null!;


}

Register the database context


In ASP.NET Core, services such as the DB context must be registered with the
dependency injection (DI) container. The container provides the service to controllers.

Update Program.cs with the following highlighted code:

C#

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();
app.MapControllers();

app.Run();

The preceding code:

Adds using directives.


Adds the database context to the DI container.
Specifies that the database context will use an in-memory database.

Scaffold a controller
Visual Studio

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.

If the scaffolding operation fails, select Add to try scaffolding a second time.

The generated code:

Marks 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.

The ASP.NET Core templates for:

Controllers with views include [action] in the route template.


API controllers don't include [action] in the route template.
When the [action] token isn't in the route template, the action name (method name)
isn't included in the endpoint. That is, the action's associated method name isn't used in
the matching route.

Update the PostTodoItem create method


Update the return statement in the PostTodoItem to use the nameof operator:

C#

[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();

// return CreatedAtAction("GetTodoItem", new { id = todoItem.Id },


todoItem);
return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id },
todoItem);
}

The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute.
The method gets the value of the TodoItem from the body of the HTTP request.

For more information, see Attribute routing with Http[Verb] attributes.

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.

Test PostTodoItem
Press Ctrl+F5 to run the app.

In the Swagger browser window, select POST /api/TodoItems, and then select Try
it out.
In the Request body input window, update the JSON. For example,

JSON

{
"name": "walk dog",
"isComplete": true
}

Select Execute
Test the location header URI
In the preceding POST, the Swagger UI shows the location header under Response
headers. For example, location: https://localhost:7260/api/TodoItems/1 . The location
header shows the URI to the created resource.

To test the location header:

In the Swagger browser window, select GET /api/TodoItems/{id}, and then select
Try it out.

Enter 1 in the id input box, and then select Execute.


Examine the GET methods
Two GET endpoints are implemented:

GET /api/todoitems
GET /api/todoitems/{id}

The previous section showed an example of the /api/todoitems/{id} route.


Follow the POST instructions to add another todo item, and then test the
/api/todoitems route using Swagger.

This app uses an in-memory database. If the app is stopped and started, the preceding
GET request will not return any data. If no data is returned, POST data to the app.

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:

C#

[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase

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.

C#

[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 OK ,
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 status NotFound
error code.
Otherwise, the method returns 200 with a JSON response body. Returning item
results in an HTTP 200 response.

The PutTodoItem method


Examine the PutTodoItem method:

C#

[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.

Test the PutTodoItem method


This sample uses an in-memory database that must be initialized each time the app is
started. There must be an item in the database before you make a PUT call. Call GET to
ensure there's an item in the database before making a PUT call.

Using the Swagger UI, use the PUT button to update the TodoItem that has Id = 1 and
set its name to "feed fish" . Note the response is HTTP 204 No Content .

The DeleteTodoItem method


Examine the DeleteTodoItem method:

C#

[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();
}

Test the DeleteTodoItem method


Use the Swagger UI to delete the TodoItem that has Id = 1. Note the response is HTTP
204 No Content .

Test with http-repl, Postman, or curl


http-repl, Postman , and curl are often used to test API's. Swagger uses curl and
shows the curl command it submitted.

For instructions on these tools, see the following links:

Test APIs with Postman


Install and test APIs with http-repl

For more information on http-repl , see Test web APIs with the HttpRepl.

Prevent over-posting
Currently the sample app exposes the entire TodoItem object. Production apps typically
limit the data that's input and returned using a subset of the model. There are multiple
reasons behind this, and security is a major one. The subset of a model is usually
referred to as a Data Transfer Object (DTO), input model, or view model. DTO is used in
this tutorial.

A DTO may be used to:

Prevent over-posting.
Hide properties that clients are not supposed to view.
Omit some properties in order to reduce payload size.
Flatten object graphs that contain nested objects. Flattened object graphs can be
more convenient for clients.

To demonstrate the DTO approach, update the TodoItem class to include a secret field:

C#

namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
}
The secret field needs to be hidden from this app, but an administrative app could
choose to expose it.

Verify you can post and get the secret field.

Create a DTO model:

C#

namespace TodoApi.Models;

public class TodoItemDTO


{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}

Update the TodoItemsController to use TodoItemDTO :

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers;

[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
private readonly TodoContext _context;

public TodoItemsController(TodoContext context)


{
_context = context;
}

// GET: api/TodoItems
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
{
return await _context.TodoItems
.Select(x => ItemToDTO(x))
.ToListAsync();
}

// GET: api/TodoItems/5
// <snippet_GetByID>
[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null)
{
return NotFound();
}

return ItemToDTO(todoItem);
}
// </snippet_GetByID>

// PUT: api/TodoItems/5
// To protect from overposting attacks, see
https://go.microsoft.com/fwlink/?linkid=2123754
// <snippet_Update>
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO
todoDTO)
{
if (id != todoDTO.Id)
{
return BadRequest();
}

var todoItem = await _context.TodoItems.FindAsync(id);


if (todoItem == null)
{
return NotFound();
}

todoItem.Name = todoDTO.Name;
todoItem.IsComplete = todoDTO.IsComplete;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
{
return NotFound();
}

return NoContent();
}
// </snippet_Update>

// POST: api/TodoItems
// To protect from overposting attacks, see
https://go.microsoft.com/fwlink/?linkid=2123754
// <snippet_Create>
[HttpPost]
public async Task<ActionResult<TodoItemDTO>> PostTodoItem(TodoItemDTO
todoDTO)
{
var todoItem = new TodoItem
{
IsComplete = todoDTO.IsComplete,
Name = todoDTO.Name
};

_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();

return CreatedAtAction(
nameof(GetTodoItem),
new { id = todoItem.Id },
ItemToDTO(todoItem));
}
// </snippet_Create>

// DELETE: api/TodoItems/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();
}

private bool TodoItemExists(long id)


{
return _context.TodoItems.Any(e => e.Id == id);
}

private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>


new TodoItemDTO
{
Id = todoItem.Id,
Name = todoItem.Name,
IsComplete = todoItem.IsComplete
};
}

Verify you can't post or get the secret field.

Call the web API with JavaScript


See Tutorial: Call an ASP.NET Core web API with JavaScript.

Web API video series


See Video: Beginner's Series to: Web APIs.

Reliable web app patterns


See The Reliable Web App Pattern for.NET YouTube videos and article for guidance on
creating a modern, reliable, performant, testable, cost-efficient, and scalable ASP.NET
Core app, whether from scratch or refactoring an existing app.

Add authentication support to a web API


ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web
apps. To secure web APIs and SPAs, use one of the following:

Azure Active Directory


Azure Active Directory B2C (Azure AD B2C)
Duende Identity Server

Duende Identity Server is an OpenID Connect and OAuth 2.0 framework for ASP.NET
Core. Duende Identity Server enables the following security features:

Authentication as a Service (AaaS)


Single sign-on/off (SSO) over multiple application types
Access control for APIs
Federation Gateway

) Important

Duende Software might require you to pay a license fee for production use of
Duende Identity Server. For more information, see Migrate from ASP.NET Core 5.0
to 6.0.

For more information, see the Duende Identity Server documentation (Duende Software
website) .

Publish to Azure
For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.

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


Tutorial: Create a minimal API with ASP.NET Core
ASP.NET Core web API documentation with Swagger / OpenAPI
Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8
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
Create a web API with ASP.NET Core
Create a web API with ASP.NET Core and
MongoDB
Article • 04/13/2023

By Pratik Khandelwal and Scott Addie

This tutorial creates a web API that runs Create, Read, Update, and Delete (CRUD)
operations on a MongoDB NoSQL database.

In this tutorial, you learn how to:

" Configure MongoDB
" Create a MongoDB database
" Define a MongoDB collection and schema
" Perform MongoDB CRUD operations from a web API
" Customize JSON serialization

Prerequisites
MongoDB 6.0.5 or later
MongoDB Shell

Visual Studio

Visual Studio 2022 with the ASP.NET and web development workload.
Configure MongoDB
Enable MongoDB and Mongo DB Shell access from anywhere on the development
machine:

1. On 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.

2. Download the MongoDB Shell and choose a directory to extract it to. Add the
resulting path for mongosh.exe to the PATH environment variable.

3. Choose a directory on the 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.

4. In the OS command shell (not the MongoDB Shell), use the following command to
connect to MongoDB on default port 27017. Replace <data_directory_path> with
the directory chosen in the previous step.

Console

mongod --dbpath <data_directory_path>

Use the previously installed MongoDB Shell in the following steps to create a database,
make collections, and store documents. For more information on MongoDB Shell
commands, see mongosh .

1. Open a MongoDB command shell instance by launching mongosh.exe .

2. In the command shell connect to the default test database by running the
following command:

Console

mongosh

3. Run the following command in the command shell:

Console

use BookStore
A database named BookStore is created if it doesn't already exist. If the database
does exist, its connection is opened for transactions.

4. Create a Books collection using following command:

Console

db.createCollection('Books')

The following result is displayed:

Console

{ "ok" : 1 }

5. Define a schema for the Books collection and insert two documents using the
following command:

Console

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" }])

A result similar to the following is displayed:

Console

{
"acknowledged" : true,
"insertedIds" : [
ObjectId("61a6058e6c43f32854e51f51"),
ObjectId("61a6058e6c43f32854e51f52")
]
}

7 Note

The ObjectId s shown in the preceding result won't match those shown in the
command shell.

6. View the documents in the database using the following command:


Console

db.Books.find().pretty()

A result similar to the following is displayed:

Console

{
"_id" : ObjectId("61a6058e6c43f32854e51f51"),
"Name" : "Design Patterns",
"Price" : 54.93,
"Category" : "Computers",
"Author" : "Ralph Johnson"
}
{
"_id" : ObjectId("61a6058e6c43f32854e51f52"),
"Name" : "Clean Code",
"Price" : 43.15,
"Category" : "Computers",
"Author" : "Robert C. Martin"
}

The schema adds an autogenerated _id property of type ObjectId for each
document.

Create the ASP.NET Core web API project


Visual Studio

1. Go to File > New > Project.

2. Select the ASP.NET Core Web API project type, and select Next.

3. Name the project BookStoreApi, and select Next.

4. Select the .NET 7.0 (Standard Term Support) framework and select Create.

5. From the Tools menu, select NuGet Package Manager > Package Manager
Console.

6. In the Package Manager Console window, navigate to the project root. Run
the following command to install the .NET driver for MongoDB:

PowerShell
Install-Package MongoDB.Driver

Add an entity model


1. Add a Models directory to the project root.

2. Add a Book class to the Models directory with the following code:

C#

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace BookStoreApi.Models;

public class Book


{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }

[BsonElement("Name")]
public string BookName { get; set; } = null!;

public decimal Price { get; set; }

public string Category { get; set; } = null!;

public string Author { get; set; } = null!;


}

In the preceding class, the Id property is:

Required for mapping the Common Language Runtime (CLR) object to the
MongoDB collection.
Annotated with [BsonId] to make this property the document's primary key.
Annotated with [BsonRepresentation(BsonType.ObjectId)] to allow passing
the parameter as type string instead of an ObjectId structure. Mongo
handles the conversion from string to ObjectId .

The BookName property is annotated with the [BsonElement] attribute. The


attribute's value of Name represents the property name in the MongoDB collection.

Add a configuration model


1. Add the following database configuration values to appsettings.json :

JSON

{
"BookStoreDatabase": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BookStore",
"BooksCollectionName": "Books"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

2. Add a BookStoreDatabaseSettings class to the Models directory with the following


code:

C#

namespace BookStoreApi.Models;

public class BookStoreDatabaseSettings


{
public string ConnectionString { get; set; } = null!;

public string DatabaseName { get; set; } = null!;

public string BooksCollectionName { get; set; } = null!;


}

The preceding BookStoreDatabaseSettings class is used to store the


appsettings.json file's BookStoreDatabase property values. The JSON and C#

property names are named identically to ease the mapping process.

3. Add the following highlighted code to Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.Configure<BookStoreDatabaseSettings>(
builder.Configuration.GetSection("BookStoreDatabase"));
In the preceding code, the configuration instance to which the appsettings.json
file's BookStoreDatabase section binds is registered in the Dependency Injection
(DI) container. For example, the BookStoreDatabaseSettings object's
ConnectionString property is populated with the
BookStoreDatabase:ConnectionString property in appsettings.json .

4. Add the following code to the top of Program.cs to resolve the


BookStoreDatabaseSettings reference:

C#

using BookStoreApi.Models;

Add a CRUD operations service


1. Add a Services directory to the project root.

2. Add a BooksService class to the Services directory with the following code:

C#

using BookStoreApi.Models;
using Microsoft.Extensions.Options;
using MongoDB.Driver;

namespace BookStoreApi.Services;

public class BooksService


{
private readonly IMongoCollection<Book> _booksCollection;

public BooksService(
IOptions<BookStoreDatabaseSettings> bookStoreDatabaseSettings)
{
var mongoClient = new MongoClient(
bookStoreDatabaseSettings.Value.ConnectionString);

var mongoDatabase = mongoClient.GetDatabase(


bookStoreDatabaseSettings.Value.DatabaseName);

_booksCollection = mongoDatabase.GetCollection<Book>(
bookStoreDatabaseSettings.Value.BooksCollectionName);
}

public async Task<List<Book>> GetAsync() =>


await _booksCollection.Find(_ => true).ToListAsync();

public async Task<Book?> GetAsync(string id) =>


await _booksCollection.Find(x => x.Id ==
id).FirstOrDefaultAsync();

public async Task CreateAsync(Book newBook) =>


await _booksCollection.InsertOneAsync(newBook);

public async Task UpdateAsync(string id, Book updatedBook) =>


await _booksCollection.ReplaceOneAsync(x => x.Id == id,
updatedBook);

public async Task RemoveAsync(string id) =>


await _booksCollection.DeleteOneAsync(x => x.Id == id);
}

In the preceding code, a BookStoreDatabaseSettings 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 Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.Configure<BookStoreDatabaseSettings>(
builder.Configuration.GetSection("BookStoreDatabase"));

builder.Services.AddSingleton<BooksService>();

In the preceding code, the BooksService class is registered with DI to support


constructor injection in consuming classes. The singleton service lifetime is most
appropriate because BooksService 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 Program.cs to resolve the BooksService
reference:

C#

using BookStoreApi.Services;

The BooksService class uses the following MongoDB.Driver members to run CRUD
operations against the database:
MongoClient : Reads the server instance for running database operations. The
constructor of this class is provided the MongoDB connection string:

C#

public BooksService(
IOptions<BookStoreDatabaseSettings> bookStoreDatabaseSettings)
{
var mongoClient = new MongoClient(
bookStoreDatabaseSettings.Value.ConnectionString);

var mongoDatabase = mongoClient.GetDatabase(


bookStoreDatabaseSettings.Value.DatabaseName);

_booksCollection = mongoDatabase.GetCollection<Book>(
bookStoreDatabaseSettings.Value.BooksCollectionName);
}

IMongoDatabase : Represents the Mongo database for running operations. This


tutorial uses the generic GetCollection<TDocument>(collection) method on the
interface to gain access to data in a specific collection. Run CRUD operations
against the collection after this method is called. In the GetCollection<TDocument>
(collection) method call:

collection represents the collection name.

TDocument represents the CLR object type stored in the collection.

GetCollection<TDocument>(collection) returns a MongoCollection object


representing the collection. In this tutorial, the following methods are invoked on the
collection:

DeleteOneAsync : Deletes a single document matching the provided search


criteria.
Find<TDocument> : Returns all documents in the collection matching the
provided search criteria.
InsertOneAsync : Inserts the provided object as a new document in the collection.
ReplaceOneAsync : Replaces the single document matching the provided search
criteria with the provided object.

Add a controller
Add a BooksController class to the Controllers directory with the following code:

C#
using BookStoreApi.Models;
using BookStoreApi.Services;
using Microsoft.AspNetCore.Mvc;

namespace BookStoreApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly BooksService _booksService;

public BooksController(BooksService booksService) =>


_booksService = booksService;

[HttpGet]
public async Task<List<Book>> Get() =>
await _booksService.GetAsync();

[HttpGet("{id:length(24)}")]
public async Task<ActionResult<Book>> Get(string id)
{
var book = await _booksService.GetAsync(id);

if (book is null)
{
return NotFound();
}

return book;
}

[HttpPost]
public async Task<IActionResult> Post(Book newBook)
{
await _booksService.CreateAsync(newBook);

return CreatedAtAction(nameof(Get), new { id = newBook.Id },


newBook);
}

[HttpPut("{id:length(24)}")]
public async Task<IActionResult> Update(string id, Book updatedBook)
{
var book = await _booksService.GetAsync(id);

if (book is null)
{
return NotFound();
}

updatedBook.Id = book.Id;

await _booksService.UpdateAsync(id, updatedBook);


return NoContent();
}

[HttpDelete("{id:length(24)}")]
public async Task<IActionResult> Delete(string id)
{
var book = await _booksService.GetAsync(id);

if (book is null)
{
return NotFound();
}

await _booksService.RemoveAsync(id);

return NoContent();
}
}

The preceding web API controller:

Uses the BooksService class to run CRUD operations.


Contains action methods to support GET, POST, PUT, and DELETE HTTP requests.
Calls CreatedAtAction in the Create action method to return an HTTP 201
response. Status code 201 is the standard response for an HTTP POST method that
creates a new resource on the server. CreatedAtAction also adds a Location
header to the response. The Location header specifies the URI of the newly
created book.

Test the web API


1. Build and run the app.

2. Navigate to https://localhost:<port>/api/books , where <port> is the


automatically assigned port number for the app, to test the controller's
parameterless Get action method. A JSON response similar to the following is
displayed:

JSON

[
{
"id": "61a6058e6c43f32854e51f51",
"bookName": "Design Patterns",
"price": 54.93,
"category": "Computers",
"author": "Ralph Johnson"
},
{
"id": "61a6058e6c43f32854e51f52",
"bookName": "Clean Code",
"price": 43.15,
"category": "Computers",
"author": "Robert C. Martin"
}
]

3. Navigate to https://localhost:<port>/api/books/{id here} to test the controller's


overloaded Get action method. A JSON response similar to the following is
displayed:

JSON

{
"id": "61a6058e6c43f32854e51f52",
"bookName": "Clean Code",
"price": 43.15,
"category": "Computers",
"author": "Robert C. Martin"
}

Configure JSON serialization options


There are two details to change about the JSON responses returned in the Test the web
API section:

The property names' default camel casing should be changed to match the Pascal
casing of the CLR object's property names.
The bookName property should be returned as Name .

To satisfy the preceding requirements, make the following changes:

1. In Program.cs , chain the following highlighted code on to the AddControllers


method call:

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.Configure<BookStoreDatabaseSettings>(
builder.Configuration.GetSection("BookStoreDatabase"));
builder.Services.AddSingleton<BooksService>();

builder.Services.AddControllers()
.AddJsonOptions(
options => options.JsonSerializerOptions.PropertyNamingPolicy =
null);

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 instead of author .

2. In Models/Book.cs , annotate the BookName property with the [JsonPropertyName]


attribute:

C#

[BsonElement("Name")]
[JsonPropertyName("Name")]
public string BookName { get; set; } = null!;

The [JsonPropertyName] 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:

C#

using System.Text.Json.Serialization;

4. Repeat the steps defined in the Test the web API section. Notice the difference in
JSON property names.

Add authentication support to a web API


ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web
apps. To secure web APIs and SPAs, use one of the following:

Azure Active Directory


Azure Active Directory B2C (Azure AD B2C)
Duende Identity Server

Duende Identity Server is an OpenID Connect and OAuth 2.0 framework for ASP.NET
Core. Duende Identity Server enables the following security features:
Authentication as a Service (AaaS)
Single sign-on/off (SSO) over multiple application types
Access control for APIs
Federation Gateway

) Important

Duende Software might require you to pay a license fee for production use of
Duende Identity Server. For more information, see Migrate from ASP.NET Core 5.0
to 6.0.

For more information, see the Duende Identity Server documentation (Duende Software
website) .

Additional resources
View or download sample code (how to download)
Create web APIs with ASP.NET Core
Controller action return types in ASP.NET Core web API
Create a web API with ASP.NET Core
Tutorial: Call an ASP.NET Core web API
with JavaScript
Article • 12/02/2022

By Rick Anderson

This tutorial shows how to call an ASP.NET Core web API with JavaScript, using the Fetch
API .

Prerequisites
Complete Tutorial: Create a web API
Familiarity with CSS, HTML, and JavaScript

Call the web API with JavaScript


In this section, you'll add an HTML page containing forms for creating and managing to-
do items. Event handlers are attached to elements on the page. The event handlers
result in HTTP requests to the web API's action methods. The Fetch API's fetch function
initiates each HTTP request.

The fetch function returns a Promise object, which contains an HTTP response
represented as a Response object. A common pattern is to extract the JSON response
body by invoking the json function on the Response object. JavaScript updates the
page with the details from the web API's response.

The simplest fetch call accepts a single parameter representing the route. A second
parameter, known as the init object, is optional. init is used to configure the HTTP
request.

1. Configure the app to serve static files and enable default file mapping. The
following highlighted code is needed in Program.cs :

C#

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));

var app = builder.Build();

if (builder.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseDefaultFiles();
app.UseStaticFiles();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

1. Create a wwwroot folder in the project root.

2. Create a css folder inside of the wwwroot folder.

3. Create a js folder inside of the wwwroot folder.

4. Add an HTML file named index.html to the wwwroot folder. Replace the contents
of index.html with the following markup:

HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<link rel="stylesheet" href="css/site.css" />
</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="editForm">
<h3>Edit</h3>
<form action="javascript:void(0);" onsubmit="updateItem()">
<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">&#10006;</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="js/site.js" asp-append-version="true"></script>


<script type="text/javascript">
getItems();
</script>
</body>
</html>

5. Add a CSS file named site.css to the wwwroot/css folder. Replace the contents of
site.css with the following styles:

css

input[type='submit'], button, [aria-label] {


cursor: pointer;
}

#editForm {
display: none;
}

table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}

th {
background-color: #f8f8f8;
padding: 5px;
}

td {
border: 1px solid;
padding: 5px;
}

6. Add a JavaScript file named site.js to the wwwroot/js folder. Replace the
contents of site.js with the following code:

JavaScript

const uri = 'api/todoitems';


let todos = [];

function getItems() {
fetch(uri)
.then(response => response.json())
.then(data => _displayItems(data))
.catch(error => console.error('Unable to get items.', error));
}

function addItem() {
const addNameTextbox = document.getElementById('add-name');

const item = {
isComplete: false,
name: addNameTextbox.value.trim()
};

fetch(uri, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
})
.then(response => response.json())
.then(() => {
getItems();
addNameTextbox.value = '';
})
.catch(error => console.error('Unable to add item.', error));
}

function deleteItem(id) {
fetch(`${uri}/${id}`, {
method: 'DELETE'
})
.then(() => getItems())
.catch(error => console.error('Unable to delete item.', error));
}

function displayEditForm(id) {
const item = todos.find(item => item.id === id);
document.getElementById('edit-name').value = item.name;
document.getElementById('edit-id').value = item.id;
document.getElementById('edit-isComplete').checked = item.isComplete;
document.getElementById('editForm').style.display = 'block';
}

function updateItem() {
const itemId = document.getElementById('edit-id').value;
const item = {
id: parseInt(itemId, 10),
isComplete: document.getElementById('edit-isComplete').checked,
name: document.getElementById('edit-name').value.trim()
};

fetch(`${uri}/${itemId}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
})
.then(() => getItems())
.catch(error => console.error('Unable to update item.', error));

closeInput();

return false;
}

function closeInput() {
document.getElementById('editForm').style.display = 'none';
}

function _displayCount(itemCount) {
const name = (itemCount === 1) ? 'to-do' : 'to-dos';

document.getElementById('counter').innerText = `${itemCount}
${name}`;
}

function _displayItems(data) {
const tBody = document.getElementById('todos');
tBody.innerHTML = '';

_displayCount(data.length);

const button = document.createElement('button');

data.forEach(item => {
let isCompleteCheckbox = document.createElement('input');
isCompleteCheckbox.type = 'checkbox';
isCompleteCheckbox.disabled = true;
isCompleteCheckbox.checked = item.isComplete;
let editButton = button.cloneNode(false);
editButton.innerText = 'Edit';
editButton.setAttribute('onclick', `displayEditForm(${item.id})`);

let deleteButton = button.cloneNode(false);


deleteButton.innerText = 'Delete';
deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

let tr = tBody.insertRow();

let td1 = tr.insertCell(0);


td1.appendChild(isCompleteCheckbox);

let td2 = tr.insertCell(1);


let textNode = document.createTextNode(item.name);
td2.appendChild(textNode);

let td3 = tr.insertCell(2);


td3.appendChild(editButton);

let td4 = tr.insertCell(3);


td4.appendChild(deleteButton);
});

todos = data;
}

A change to the ASP.NET Core project's launch settings may be required to test the
HTML page locally:

1. Open Properties\launchSettings.json.
2. Remove the launchUrl property to force the app to open at index.html —the
project's default file.

This sample calls all of the CRUD methods of the web API. Following are explanations of
the web API requests.

Get a list of to-do items


In the following code, an HTTP GET request is sent to the api/todoitems route:

JavaScript

fetch(uri)
.then(response => response.json())
.then(data => _displayItems(data))
.catch(error => console.error('Unable to get items.', error));
When the web API returns a successful status code, the _displayItems function is
invoked. Each to-do item in the array parameter accepted by _displayItems is added to
a table with Edit and Delete buttons. If the web API request fails, an error is logged to
the browser's console.

Add a to-do item


In the following code:

An item variable is declared to construct an object literal representation of the to-


do item.
A Fetch request is configured with the following options:
method —specifies the POST HTTP action verb.
body —specifies the JSON representation of the request body. The JSON is

produced by passing the object literal stored in item to the JSON.stringify


function.
headers —specifies the Accept and Content-Type HTTP request headers. Both

headers are set to application/json to specify the media type being received
and sent, respectively.
An HTTP POST request is sent to the api/todoitems route.

JavaScript

function addItem() {
const addNameTextbox = document.getElementById('add-name');

const item = {
isComplete: false,
name: addNameTextbox.value.trim()
};

fetch(uri, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
})
.then(response => response.json())
.then(() => {
getItems();
addNameTextbox.value = '';
})
.catch(error => console.error('Unable to add item.', error));
}
When the web API returns a successful status code, the getItems function is invoked to
update the HTML table. If the web API request fails, an error is logged to the browser's
console.

Update a to-do item


Updating a to-do item is similar to adding one; however, there are two significant
differences:

The route is suffixed with the unique identifier of the item to update. For example,
api/todoitems/1.
The HTTP action verb is PUT, as indicated by the method option.

JavaScript

fetch(`${uri}/${itemId}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
})
.then(() => getItems())
.catch(error => console.error('Unable to update item.', error));

Delete a to-do item


To delete a to-do item, set the request's method option to DELETE and specify the item's
unique identifier in the URL.

JavaScript

fetch(`${uri}/${id}`, {
method: 'DELETE'
})
.then(() => getItems())
.catch(error => console.error('Unable to delete item.', error));

Advance to the next tutorial to learn how to generate web API help pages:

Get started with Swashbuckle and ASP.NET Core


Create backend services for native
mobile apps with ASP.NET Core
Article • 09/21/2022

By James Montemagno

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

The Sample Native Mobile App


This tutorial demonstrates how to create backend services using ASP.NET Core to
support native mobile apps. It uses the Xamarin.Forms TodoRest app as its native client,
which includes separate native clients for Android, iOS, and Windows. You can follow the
linked tutorial to create the native app (and install the necessary free Xamarin tools), as
well as download the Xamarin sample solution. The Xamarin sample includes an
ASP.NET Core Web API services project, which this article's ASP.NET Core app replaces
(with no changes required by the client).
Features
The TodoREST app supports listing, adding, deleting, and updating To-Do items. Each
item has an ID, a Name, Notes, and a property indicating whether it's been Done yet.

The main view of the items, as shown above, lists each item's name and indicates if it's
done with a checkmark.

Tapping the + icon opens an add item dialog:


Tapping an item on the main list screen opens up an edit dialog where the item's Name,
Notes, and Done settings can be modified, or the item can be deleted:
To test it out yourself against the ASP.NET Core app created in the next section running
on your computer, update the app's RestUrl constant.

Android emulators do not run on the local machine and use a loopback IP (10.0.2.2) to
communicate with the local machine. Leverage Xamarin.Essentials DeviceInfo to detect
what operating the system is running to use the correct URL.

Navigate to the TodoREST project and open the Constants.cs file. The Constants.cs
file contains the following configuration.

C#
using Xamarin.Essentials;
using Xamarin.Forms;

namespace TodoREST
{
public static class Constants
{
// URL of REST service
//public static string RestUrl =
"https://YOURPROJECT.azurewebsites.net:8081/api/todoitems/{0}";

// URL of REST service (Android does not use localhost)


// Use http cleartext for local deployment. Change to https for
production
public static string RestUrl = DeviceInfo.Platform ==
DevicePlatform.Android ? "http://10.0.2.2:5000/api/todoitems/{0}" :
"http://localhost:5000/api/todoitems/{0}";
}
}

You can optionally deploy the web service to a cloud service such as Azure and update
the RestUrl .

Creating the ASP.NET Core Project


Create a new ASP.NET Core Web Application in Visual Studio. Choose the Web API
template. Name the project TodoAPI.
The app should respond to all requests made to port 5000 including clear-text http
traffic for our mobile client. Update Startup.cs so UseHttpsRedirection doesn't run in
development:

C#

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// For mobile apps, allow http traffic.
app.UseHttpsRedirection();
}

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

7 Note

Run the app directly, rather than behind IIS Express. IIS Express ignores non-local
requests by default. Run dotnet run from a command prompt, or choose the app
name profile from the Debug Target dropdown in the Visual Studio toolbar.

Add a model class to represent To-Do items. Mark required fields with the [Required]
attribute:

C#

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

public bool Done { get; set; }


}
}

The API methods require some way to work with data. Use the same ITodoRepository
interface the original Xamarin sample uses:

C#

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:

C#

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

public IEnumerable<TodoItem> All


{
get { return _todoList; }
}

public bool DoesItemExist(string id)


{
return _todoList.Any(item => item.ID == id);
}

public TodoItem Find(string id)


{
return _todoList.FirstOrDefault(item => item.ID == id);
}

public void Insert(TodoItem item)


{
_todoList.Add(item);
}

public void Update(TodoItem item)


{
var todoItem = this.Find(item.ID);
var index = _todoList.IndexOf(todoItem);
_todoList.RemoveAt(index);
_todoList.Insert(index, item);
}
public void Delete(string id)
{
_todoList.Remove(this.Find(id));
}

private void InitializeData()


{
_todoList = new List<TodoItem>();

var todoItem1 = new TodoItem


{
ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
Name = "Learn app development",
Notes = "Take Microsoft Learn Courses",
Done = true
};

var todoItem2 = new TodoItem


{
ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
Name = "Develop apps",
Notes = "Use Visual Studio and Visual Studio for Mac",
Done = false
};

var todoItem3 = new TodoItem


{
ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
Name = "Publish apps",
Notes = "All app stores",
Done = false,
};

_todoList.Add(todoItem1);
_todoList.Add(todoItem2);
_todoList.Add(todoItem3);
}
}
}

Configure the implementation in Startup.cs :

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddSingleton<ITodoRepository, TodoRepository>();
services.AddControllers();
}
Creating the Controller
Add a new controller to the project, TodoItemsController . It should inherit from
ControllerBase. Add a Route attribute to indicate that the controller will handle requests
made to paths starting with api/todoitems . The [controller] token in the route is
replaced by the name of the controller (omitting the Controller suffix), and is especially
helpful for global routes. Learn more about routing.

The controller requires an ITodoRepository to function; request an instance of this type


through the controller's constructor. At runtime, this instance will be provided using the
framework's support for dependency injection.

C#

[ApiController]
[Route("api/[controller]")]
public class TodoItemsController : ControllerBase
{
private readonly ITodoRepository _todoRepository;

public TodoItemsController(ITodoRepository todoRepository)


{
_todoRepository = 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.

C#

[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 is passed in the body of the POST, this parameter specifies 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.

C#

[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:

C#

public enum ErrorCode


{
TodoItemNameAndNotesRequired,
TodoItemIDInUse,
RecordNotFound,
CouldNotCreateItem,
CouldNotUpdateItem,
CouldNotDeleteItem
}

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.

C#

[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.

C#

[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.
Prevent over-posting
Currently the sample app exposes the entire TodoItem object. Production apps typically
limit the data that's input and returned using a subset of the model. There are multiple
reasons behind this and security is a major one. The subset of a model is usually referred
to as a Data Transfer Object (DTO), input model, or view model. DTO is used in this
article.

A DTO may be used to:

Prevent over-posting.
Hide properties that clients are not supposed to view.
Omit some properties in order to reduce payload size.
Flatten object graphs that contain nested objects. Flattened object graphs can be
more convenient for clients.

To demonstrate the DTO approach, see Prevent over-posting


Common Web API Conventions
As you develop the backend services for your app, you will want to come up with a
consistent set of conventions or policies for handling cross-cutting concerns. For
example, in the service shown above, requests for specific records that weren't found
received a NotFound response, rather than a BadRequest response. Similarly, commands
made to this service that passed in model bound types always checked
ModelState.IsValid and returned a BadRequest for invalid model types.

Once you've identified a common policy for your APIs, you can usually encapsulate it in
a filter. Learn more about how to encapsulate common API policies in ASP.NET Core
MVC applications.

Additional resources
Xamarin.Forms: Web Service Authentication
Xamarin.Forms: Consume a RESTful Web Service
Consume REST web services in Xamarin Apps
Create a web API with ASP.NET Core
Publish an ASP.NET Core web API to
Azure API Management with Visual
Studio
Article • 11/04/2022

By Matt Soucoup

In this tutorial you'll learn how to create an ASP.NET Core web API project using Visual
Studio, ensure it has OpenAPI support, and then publish the web API to both Azure App
Service and Azure API Management.

Set up
To complete the tutorial you'll need an Azure account.

Open a free Azure account if you don't have one.

Create an ASP.NET Core web API


Visual Studio allows you to easily create a new ASP.NET Core web API project from a
template. Follow these directions to create a new ASP.NET Core web API project:

From the File menu, select New > Project.


Enter Web API in the search box.
Select the ASP.NET Core Web API template and select Next.
In the Configure your new project dialog, name the project WeatherAPI and
select Next.
In the Additional information dialog:
Confirm the Framework is .NET 6.0 (Long-term support).
Confirm the checkbox for Use controllers (uncheck to use minimal APIs) is
checked.
Confirm the checkbox for Enable OpenAPI support is checked.
Select Create.

Explore the code


Swagger definitions allow Azure API Management to read the app's API definitions. By
checking the Enable OpenAPI support checkbox during app creation, Visual Studio
automatically adds the code to create the Swagger definitions. Open up the Program.cs
file which shows the following code:

C#

...

builder.Services.AddSwaggerGen();

...

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

...

Ensure the Swagger definitions are always generated


Azure API Management needs the Swagger definitions to always be present, regardless
of the application's environment. To ensure they are always generated, move
app.UseSwagger(); outside of the if (app.Environment.IsDevelopment()) block.

The updated code:

C#

...

app.UseSwagger();

if (app.Environment.IsDevelopment())
{
app.UseSwaggerUI();
}

...

Change the API routing


Change the URL structure needed to access the Get action of the
WeatherForecastController . Complete the following steps:

1. Open the WeatherForecastController.cs file.

2. Replace the [Route("[controller]")] class-level attribute with [Route("/")] . The


updated class definition :

C#

[ApiController]
[Route("/")]
public class WeatherForecastController : ControllerBase

Publish the web API to Azure App Service


Complete the following steps to publish the ASP.NET Core web API to Azure API
Management:

1. Publish the API app to Azure App Service.


2. Publish the ASP.NET Core web API app to the Azure API Management service
instance.

Publish the API app to Azure App Service


Complete the following steps to publish the ASP.NET Core web API to Azure API
Management:

1. In Solution Explorer, right-click the project and select Publish.

2. In the Publish dialog, select Azure and select the Next button.

3. Select Azure App Service (Windows) and select the Next button.

4. Select Create a new Azure App Service.

The Create App Service dialog appears. The App Name, Resource Group, and App
Service Plan entry fields are populated. You can keep these names or change
them.

5. Select the Create button.

6. Once the app service is created, select the Next button.


7. Select Create a new API Management Service.

The Create API Management Service dialog appears. You can leave the API Name,
Subscription Name, and Resource Group entry fields as they are. Select the new
button next to the API Management Service entry and enter the required fields
from that dialog box.

Select the OK button to create the API Management service.

8. Select the Create button to proceed with the API Management service creation.
This step may take several minutes to complete.

9. When that completes, select the Finish button.

10. The dialog closes and a summary screen appears with information about the
publish. Select the Publish button.

The web API publishes to both Azure App Service and Azure API Management. A
new browser window will appear and show the API running in Azure App Service.
You can close that window.

11. Open up the Azure portal in a web browser and navigate to the API Management
instance you created.

12. Select the APIs option from the left-hand menu.

13. Select the API you created in the preceding steps. It's now populated and you can
explore around.

Configure the published API name


Notice the name of the API is named WeatherAPI; however, we would like to call it
Weather Forecasts. Complete the following steps to update the name:

1. Add the following to Program.cs immediately after servies.AddSwaggerGen();

C#

builder.Services.ConfigureSwaggerGen(setup =>
{
setup.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "Weather Forecasts",
Version = "v1"
});
});
2. Republish the ASP.NET Core web API and open the Azure API Management
instance in the Azure portal.

3. Refresh the page in your browser. You'll see the name of the API is now correct.

Verify the web API is working


You can test the deployed ASP.NET Core web API in Azure API Management from the
Azure portal with the following steps:

1. Open the Test tab.


2. Select / or the Get operation.
3. Select Send.

Clean up
When you've finished testing the app, go to the Azure portal and delete the app.

1. Select Resource groups, then select the resource group you created.

2. In the Resource groups page, select Delete.

3. Enter the name of the resource group and select Delete. Your app and all other
resources created in this tutorial are now deleted from Azure.

Additional resources
Azure API Management
Azure App Service
Tutorial: Create a minimal API with
ASP.NET Core
Article • 05/19/2023

By Rick Anderson and Tom Dykstra

Minimal APIs are architected to create HTTP APIs with minimal dependencies. They are
ideal for microservices and apps that want to include only the minimum files, features,
and dependencies in ASP.NET Core.

This tutorial teaches the basics of building a minimal API with ASP.NET Core. Another
approach to creating APIs in ASP.NET Core is to use controllers. For help in choosing
between minimal APIs and controller-based APIs, see APIs overview. For a tutorial on
creating an API project based on controllers that contains more features, see Create a
web API.

Overview
This tutorial creates the following API:

API Description Request body Response body

GET /todoitems Get all to-do items None Array of to-do items

GET /todoitems/complete Get completed to-do items None Array of to-do items

GET /todoitems/{id} Get an item by ID None To-do item

POST /todoitems Add a new item To-do item To-do item

PUT /todoitems/{id} Update an existing item To-do item None

DELETE /todoitems/{id} Delete an item None None

Prerequisites
Visual Studio

Visual Studio 2022 with the ASP.NET and web development workload.
Create an API project
Visual Studio

Start Visual Studio 2022 and select Create a new project.

In the Create a new project dialog:


Enter Empty in the Search for templates search box.
Select the ASP.NET Core Empty template and select Next.
Name the project TodoApi and select Next.

In the Additional information dialog:


Select .NET 7.0
Uncheck Do not use top-level statements
Select Create
Examine the code
The Program.cs file contains the following code:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

The preceding code:

Creates a WebApplicationBuilder and a WebApplication with preconfigured


defaults.
Creates an HTTP GET endpoint / that returns Hello World! :

Run the app

Visual Studio

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog:

Select Yes if you trust the IIS Express SSL certificate.

The following dialog is displayed:


Select Yes if you agree to trust the development certificate.

For information on trusting the Firefox browser, see Firefox


SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

Visual Studio launches the Kestrel web server and opens a browser window.

Hello World! is displayed in the browser. The Program.cs file contains a minimal but

complete app.

Add NuGet packages


NuGet packages must be added to support the database and diagnostics used in this
tutorial.

Visual Studio

From the Tools menu, select NuGet Package Manager > Manage NuGet
Packages for Solution.
Select the Browse tab.
Enter Microsoft.EntityFrameworkCore.InMemory in the search box, and then
select Microsoft.EntityFrameworkCore.InMemory .
Select the Project checkbox in the right pane and then select Install.
Follow the preceding instructions to add the
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore package.

The model and database context classes


In the project folder, create a file named Todo.cs with the following code:

C#

public class Todo


{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}

The preceding code creates the model for this app. A model is a class that represents
data that the app manages.

Create a file named TodoDb.cs with the following code:

C#

using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext


{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }

public DbSet<Todo> Todos => Set<Todo>();


}

The preceding code defines the database context, which is the main class that
coordinates Entity Framework functionality for a data model. This class derives from the
Microsoft.EntityFrameworkCore.DbContext class.

Add the API code


Replace the contents of the Program.cs file with the following code:

C#
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>


await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>


await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>


await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>


{
db.Todos.Add(todo);
await db.SaveChangesAsync();

return Results.Created($"/todoitems/{todo.Id}", todo);


});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>


{
var todo = await db.Todos.FindAsync(id);

if (todo is null) return Results.NotFound();

todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;

await db.SaveChangesAsync();

return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>


{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}

return Results.NotFound();
});
app.Run();

The following highlighted code adds the database context to the dependency injection
(DI) container and enables displaying database-related exceptions:

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

The DI container provides access to the database context and other services.

Install Postman to test the app


This tutorial uses Postman to test the API.

Install Postman
Start the web app.
Start Postman.
Disable SSL certificate verification
For Postman for Windows, Select File > Settings (General tab), disable SSL
certificate verification.
For Postman for macOS, Select Postman > Preferences (General tab), disable
SSL certificate verification.

2 Warning

Re-enable SSL certificate verification after testing the sample app.

Test posting data


The following code in Program.cs creates an HTTP POST endpoint /todoitems that adds
data to the in-memory database:

C#

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>


{
db.Todos.Add(todo);
await db.SaveChangesAsync();

return Results.Created($"/todoitems/{todo.Id}", todo);


});

Run the app. The browser displays a 404 error because there is no longer a / endpoint.

Use the POST endpoint to add data to the app:

Create a new HTTP request.

Set the HTTP method to POST .

Set the URI to https://localhost:<port>/todoitems . For example:


https://localhost:5001/todoitems

Select the Body tab.

Select raw.

Set the type to JSON.

In the request body enter JSON for a to-do item:

JSON

{
"name":"walk dog",
"isComplete":true
}

Select Send.
Examine the GET endpoints
The sample app implements several GET endpoints by calling MapGet :

API Description Request body Response body

GET /todoitems Get all to-do items None Array of to-do items

GET /todoitems/complete Get all completed to-do items None Array of to-do items

GET /todoitems/{id} Get an item by ID None To-do item

C#

app.MapGet("/todoitems", async (TodoDb db) =>


await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>


await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>


await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());

Test the GET endpoints


Test the app by calling the endpoints from a browser or Postman. The following steps
are for Postman.

Create a new HTTP request.


Set the HTTP method to GET.
Set the request URI to https://localhost:<port>/todoitems . For example,
https://localhost:5001/todoitems .

Select Send.

The call to GET /todoitems produces a response similar to the following:

JSON

[
{
"id": 1,
"name": "walk dog",
"isComplete": false
}
]

Set the request URI to https://localhost:<port>/todoitems/1 . For example,


https://localhost:5001/todoitems/1 .
Select Send.
The response is similar to the following:

JSON

{
"id": 1,
"name": "walk dog",
"isComplete": false
}

This app uses an in-memory database. If the app is restarted, the GET request doesn't
return any data. If no data is returned, POST data to the app and try the GET request
again.
Return values
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 OK ,
assuming there are no unhandled exceptions. Unhandled exceptions are translated into
5xx errors.

The return types can represent a wide range of HTTP status codes. For example, GET
/todoitems/{id} can return two different status values:

If no item matches the requested ID, the method returns a 404 status NotFound
error code.
Otherwise, the method returns 200 with a JSON response body. Returning item
results in an HTTP 200 response.

Examine the PUT endpoint


The sample app implements a single PUT endpoint using MapPut :

C#

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>


{
var todo = await db.Todos.FindAsync(id);

if (todo is null) return Results.NotFound();

todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;

await db.SaveChangesAsync();

return Results.NoContent();
});

This method is similar to the MapPost method, except it uses HTTP PUT. A successful
response returns 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.

Test the PUT endpoint


This sample uses an in-memory database that must be initialized each time the app is
started. There must be an item in the database before you make a PUT call. Call GET to
ensure 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" :

JSON

{
"id": 1,
"name": "feed fish",
"isComplete": false
}

Examine and test the DELETE endpoint


The sample app implements a single DELETE endpoint using MapDelete :

C#

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>


{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}

return Results.NotFound();
});

Use Postman to delete a to-do item:

Set the method to DELETE .


Set the URI of the object to delete (for example
https://localhost:5001/todoitems/1 ).
Select Send.

Use the MapGroup API


The sample app code repeats the todoitems URL prefix each time it sets up an endpoint.
APIs often have groups of endpoints with a common URL prefix, and the MapGroup
method is available to help organize such groups. It reduces repetitive code and allows
for customizing entire groups of endpoints with a single call to methods like
RequireAuthorization and WithMetadata.
Replace the contents of Program.cs with the following code:

C#

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>


await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>


await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>


await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>


{
db.Todos.Add(todo);
await db.SaveChangesAsync();

return Results.Created($"/todoitems/{todo.Id}", todo);


});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>


{
var todo = await db.Todos.FindAsync(id);

if (todo is null) return Results.NotFound();

todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;

await db.SaveChangesAsync();

return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>


{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}

return Results.NotFound();
});

app.Run();

The preceding code has the following changes:

Adds var todoItems = app.MapGroup("/todoitems"); to set up the group using the


URL prefix /todoitems .
Changes all the app.Map<HttpVerb> methods to todoItems.Map<HttpVerb> .
Removes the URL prefix /todoitems from the Map<HttpVerb> method calls.

Test the endpoints to verify that they work the same.

Use the TypedResults API


Returning TypedResults rather than Results has several advantages, including testability
and automatically returning the response type metadata for OpenAPI to describe the
endpoint. For more information, see TypedResults vs Results.

The Map<HttpVerb> methods can call route handler methods instead of using lambdas.
To see an example, update Program.cs with the following code:

C#

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)


{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)


{
return TypedResults.Ok(await db.Todos.Where(t =>
t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)


{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)


{
db.Todos.Add(todo);
await db.SaveChangesAsync();

return TypedResults.Created($"/todoitems/{todo.Id}", todo);


}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)


{
var todo = await db.Todos.FindAsync(id);

if (todo is null) return TypedResults.NotFound();

todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;

await db.SaveChangesAsync();

return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)


{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.Ok(todo);
}

return TypedResults.NotFound();
}

The Map<HttpVerb> code now calls methods instead of lambdas:


C#

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

These methods return objects that implement IResult and are defined by TypedResults:

C#

static async Task<IResult> GetAllTodos(TodoDb db)


{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)


{
return TypedResults.Ok(await db.Todos.Where(t =>
t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)


{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)


{
db.Todos.Add(todo);
await db.SaveChangesAsync();

return TypedResults.Created($"/todoitems/{todo.Id}", todo);


}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)


{
var todo = await db.Todos.FindAsync(id);

if (todo is null) return TypedResults.NotFound();

todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;

await db.SaveChangesAsync();
return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)


{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.Ok(todo);
}

return TypedResults.NotFound();
}

Unit tests can call these methods and test that they return the correct type. For example,
if the method is GetAllTodos :

C#

static async Task<IResult> GetAllTodos(TodoDb db)


{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

Unit test code can verify that an object of type Ok<Todo[]> is returned from the handler
method. For example:

C#

public async Task GetAllTodos_ReturnsOkOfTodosResult()


{
// Arrange
var db = CreateDbContext();

// Act
var result = await TodosApi.GetAllTodos(db);

// Assert: Check for the correct returned type


Assert.IsType<Ok<Todo[]>>(result);
}

Prevent over-posting
Currently the sample app exposes the entire Todo object. Production apps typically limit
the data that's input and returned using a subset of the model. There are multiple
reasons behind this and security is a major one. The subset of a model is usually referred
to as a Data Transfer Object (DTO), input model, or view model. DTO is used in this
article.

A DTO may be used to:

Prevent over-posting.
Hide properties that clients are not supposed to view.
Omit some properties in order to reduce payload size.
Flatten object graphs that contain nested objects. Flattened object graphs can be
more convenient for clients.

To demonstrate the DTO approach, update the Todo class to include a secret field:

C#

public class Todo


{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}

The secret field needs to be hidden from this app, but an administrative app could
choose to expose it.

Verify you can post and get the secret field.

Create a file named TodoItemDTO.cs with the following code:

C#

public class TodoItemDTO


{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }

public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name,
todoItem.IsComplete);
}

Update the code in Program.cs to use this DTO model:

C#
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)


{
return TypedResults.Ok(await db.Todos.Select(x => new
TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {


return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x
=> new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)


{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)


{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};

db.Todos.Add(todoItem);
await db.SaveChangesAsync();

todoItemDTO = new TodoItemDTO(todoItem);

return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);


}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO,


TodoDb db)
{
var todo = await db.Todos.FindAsync(id);

if (todo is null) return TypedResults.NotFound();

todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;

await db.SaveChangesAsync();

return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)


{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();

TodoItemDTO todoItemDTO = new TodoItemDTO(todo);

return TypedResults.Ok(todoItemDTO);
}

return TypedResults.NotFound();
}

Verify you can post and get all fields except the secret field.

Next steps

Configure JSON serialization options


For information on how to configure JSON serialization in your Minimal API apps, see
Configure JSON serialization options.

Handle errors and exceptions


The developer exception page is enabled by default in the development environment
for minimal API apps. For information about how to handle errors and exceptions, see
Handle errors in ASP.NET Core APIs.

Test minimal API apps


For an example of testing a minimal API app, see this GitHub sample .

Use OpenAPI (Swagger)


For information on how to use OpenAPI with minimal API apps, see OpenAPI support in
minimal APIs.

Publish to Azure
For information on how to deploy to Azure, see Quickstart: Deploy an ASP.NET web app.

Learn more
For more information about minimal API apps, see Minimal APIs quick reference.
Tutorial: Get started with ASP.NET Core
SignalR
Article • 03/31/2023

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 2022 with the ASP.NET and web development workload.
Create a web app project
Visual Studio

Start Visual Studio 2022 and select Create a new project.

In the Create a new project dialog, select ASP.NET Core Web App, and then select
Next.
In the Configure your new project dialog, enter SignalRChat for Project name. It's
important to name the project SignalRChat , including matching the capitalization,
so the namespaces match the code in the tutorial.

Select Next.

In the Additional information dialog, select .NET 7.0 (Standard Term Support) and
then select Create.
Add the SignalR client library
The SignalR server library is included in the ASP.NET Core shared framework. The
JavaScript client library isn't automatically included in the project. For this tutorial, use
Library Manager (LibMan) to get the client library from unpkg . unpkg is a fast, global
content delivery network for everything on npm .

Visual Studio

In Solution Explorer, right-click the project, and select Add > Client-Side Library.

In the Add Client-Side Library dialog:

Select unpkg for Provider


Enter @microsoft/signalr@latest for Library.
Select Choose specific files, expand the dist/browser folder, and select
signalr.js and signalr.min.js .

Set Target Location to wwwroot/js/signalr/ .


Select Install.

LibMan creates a wwwroot/js/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 the ChatHub class with the following code:

C#

using Microsoft.AspNetCore.SignalR;

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 Program.cs file.

C#

using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddSignalR();
var app = builder.Build();

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
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.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();

The preceding highlighted code adds SignalR to the ASP.NET Core dependency injection
and routing systems.

Add SignalR client code


Replace the content in Pages/Index.cshtml with the following code:

CSHTML

@page
<div class="container">
<div class="row p-1">
<div class="col-1">User</div>
<div class="col-5"><input type="text" id="userInput" /></div>
</div>
<div class="row p-1">
<div class="col-1">Message</div>
<div class="col-5"><input type="text" class="w-100"
id="messageInput" /></div>
</div>
<div class="row p-1">
<div class="col-6 text-end">
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
<div class="row p-1">
<div class="col-6">
<hr />
</div>
</div>
<div class="row p-1">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>

The preceding markup:

Creates text boxes and a submit button.


Creates a list with id="messagesList" for displaying messages that are received
from the SignalR hub.
Includes script references to SignalR and the chat.js app code is created in the
next step.

In the wwwroot/js folder, create a chat.js file with the following code:

JavaScript

"use strict";

var connection = new


signalR.HubConnectionBuilder().withUrl("/chatHub").build();

//Disable the send button until connection is established.


document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {


var li = document.createElement("li");
document.getElementById("messagesList").appendChild(li);
// We can assign user-supplied strings to an element's textContent
because it
// is not interpreted as markup. If you're assigning in any other way,
you
// should be aware of possible script injection concerns.
li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function
(event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});

The preceding JavaScript:

Creates and starts a connection.


Adds to the submit button a handler that sends messages to the hub.
Adds to the connection object a handler that receives messages from the hub and
adds them to the list.

Run the app


Visual Studio

Select Ctrl + F5 to run the app without debugging.

Copy the URL from the address bar, open another browser instance or tab, and paste
the URL in the address bar.

Choose either browser, enter a name and message, and select the Send Message
button.

The name and message are displayed on both pages instantly.

 Tip
If the app doesn't work, open the browser developer tools (F12) and go to the
console. Look for possible errors related to HTML and JavaScript code. For example,
if signalr.js was put in a different folder than directed, the reference to that file
won't work resulting in a 404 error in the console.

If an ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY error has occurred in Chrome, run


the following commands to update the development certificate:

.NET CLI

dotnet dev-certs https --clean


dotnet dev-certs https --trust

Publish to Azure
For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app. For
more information on Azure SignalR Service, see What is Azure SignalR Service?.

Next steps
Use hubs
Strongly typed hubs
Authentication and authorization in ASP.NET Core SignalR
View or download sample code (how to download)
Tutorial: Get started with ASP.NET Core
SignalR using TypeScript and Webpack
Article • 03/24/2023

By Sébastien Sougnez

This tutorial demonstrates using Webpack in an ASP.NET Core SignalR web app to
bundle and build a client written in TypeScript . Webpack enables developers to
bundle and build the client-side resources of a web app.

In this tutorial, you learn how to:

" Create an ASP.NET Core SignalR app


" Configure the SignalR server
" Configure a build pipeline using Webpack
" Configure the SignalR TypeScript client
" Enable communication between the client and the server

View or download sample code (how to download)

Prerequisites
Node.js with npm

Visual Studio

Visual Studio 2022 with the ASP.NET and web development workload.
Create the ASP.NET Core web app
Visual Studio

By default, Visual Studio uses the version of npm found in its installation directory.
To configure Visual Studio to look for npm in the PATH environment variable:

Launch Visual Studio. At the start window, select Continue without code.

1. Navigate to Tools > Options > Projects and Solutions > Web Package
Management > External Web Tools.

2. Select the $(PATH) entry from the list. Select the up arrow to move the entry
to the second position in the list, and select OK:
.

To create a new ASP.NET Core web app:

1. Use the File > New > Project menu option and choose the ASP.NET Core
Empty template. Select Next.
2. Name the project SignalRWebpack , and select Create.
3. Select .NET 7.0 (Standard Term Support) from the Framework drop-down.
Select Create.

Add the Microsoft.TypeScript.MSBuild NuGet package to the project:

1. In Solution Explorer, right-click the project node and select Manage NuGet
Packages. In the Browse tab, search for Microsoft.TypeScript.MSBuild and
then select Install on the right to install the package.

Visual Studio adds the NuGet package under the Dependencies node in Solution
Explorer, enabling TypeScript compilation in the project.

Configure the server


In this section, you configure the ASP.NET Core web app to send and receive SignalR
messages.

1. In Program.cs , call AddSignalR:

C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();

2. Again, in Program.cs , call UseDefaultFiles and UseStaticFiles:

C#

var app = builder.Build();

app.UseDefaultFiles();
app.UseStaticFiles();

The preceding code allows the server to locate and serve the index.html file. The
file is served whether the user enters its full URL or the root URL of the web app.

3. Create a new directory named Hubs in the project root, SignalRWebpack/ , for the
SignalR hub class.

4. Create a new file, Hubs/ChatHub.cs , with the following code:

C#

using Microsoft.AspNetCore.SignalR;

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 is enough.

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 SendAsync on Clients.All.
The received messages are sent to all clients connected to the hub.
5. Add the following using statement at the top of Program.cs to resolve the
ChatHub reference:

C#

using SignalRWebpack.Hubs;

6. In Program.cs , map the /hub route to the ChatHub hub. Replace the code that
displays Hello World! with the following code:

C#

app.MapHub<ChatHub>("/hub");

Configure the client


In this section, you create a Node.js project to convert TypeScript to JavaScript and
bundle client-side resources, including HTML and CSS, using Webpack.

1. Run the following command in the project root to create a package.json file:

Console

npm init -y

2. Add the highlighted property to the package.json file and save the file changes:

JSON

{
"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. Run the following command from the project
root:

Console

npm i -D -E clean-webpack-plugin css-loader html-webpack-plugin mini-


css-extract-plugin ts-loader typescript webpack webpack-cli

The -E option disables npm's default behavior of writing semantic versioning


range operators to package.json . For example, "webpack": "5.76.1" is used
instead of "webpack": "^5.76.1" . This option prevents unintended upgrades to
newer package versions.

For more information, see the npm-install documentation.

4. Replace the scripts property of package.json file with the following code:

JSON

"scripts": {
"build": "webpack --mode=development --watch",
"release": "webpack --mode=production",
"publish": "npm run release && dotnet publish -c Release"
},

The following scripts are defined:

build : Bundles the client-side resources in development mode and watches

for file changes. The file watcher causes the bundle to regenerate each time a
project file changes. The mode option disables production optimizations, such
as tree shaking and minification. use build in development only.
release : Bundles the client-side resources in production mode.
publish : Runs the release script to bundle the client-side resources in

production mode. It calls the .NET CLI's publish command to publish the app.

5. Create a file named webpack.config.js in the project root, with the following code:

JavaScript

const path = require("path");


const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

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(),
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
new MiniCssExtractPlugin({
filename: "css/[name].[chunkhash].css",
}),
],
};

The preceding file configures the Webpack compilation process:

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. Copy the src directory from the sample project into the project root. The src
directory contains the following files:

index.html , which defines the homepage's boilerplate markup:

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>ASP.NET Core SignalR with TypeScript and
Webpack</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>

css/main.css , which provides CSS styles for the homepage:

css

*,
*::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;
}

tsconfig.json , which configures the TypeScript compiler to produce

ECMAScript 5-compatible JavaScript:

JSON

{
"compilerOptions": {
"target": "es5"
}
}

index.ts :

TypeScript

import * as signalR from "@microsoft/signalr";


import "./css/main.css";

const divMessages: HTMLDivElement =


document.querySelector("#divMessages");
const tbMessage: HTMLInputElement =
document.querySelector("#tbMessage");
const btnSend: HTMLButtonElement =
document.querySelector("#btnSend");
const username = new Date().getTime();

const connection = new signalR.HubConnectionBuilder()


.withUrl("/hub")
.build();

connection.on("messageReceived", (username: string, message:


string) => {
const m = document.createElement("div");

m.innerHTML = `<div class="message-author">${username}</div>


<div>${message}</div>`;

divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});

connection.start().catch((err) => document.write(err));

tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {


if (e.key === "Enter") {
send();
}
});

btnSend.addEventListener("click", send);

function send() {
connection.send("newMessage", username, tbMessage.value)
.then(() => (tbMessage.value = ""));
}

The preceding code retrieves references to DOM elements and attaches two
event handlers:
keyup : Fires when the user types in the tbMessage textbox and calls the

send function when the user presses the Enter key.


click : Fires when the user selects the Send button and calls send function

is called.

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, messages with the name
messageReceived can run the logic responsible for displaying the new
message in the messages zone. Listening to a specific message can be done
via the on function. Any number of message names can be listened to. 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.

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.

7. Run the following command at the project root:

Console

npm i @microsoft/signalr @types/node


The preceding command installs:

The SignalR TypeScript client , which allows the client to send messages to
the server.
The TypeScript type definitions for Node.js, which enables compile-time
checking of Node.js types.

Test the app


Confirm that the app works with the following steps:

Visual Studio

1. Run Webpack in release mode. Using the Package Manager Console


window, run the following command in the project root.

Console

npm run release

This command generates 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 in a process known as
transpilation.
Mangled the generated JavaScript to reduce file size in 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
https://localhost:<port> .
If there are compile errors, try closing and reopening the solution.

3. Open another browser instance (any browser) and paste the URL in the
address bar.

4. Choose either browser, type something in the Message text box, and select
the Send button. The unique user name and message are displayed on both
pages instantly.

Next steps
Strongly typed hubs
Authentication and authorization in ASP.NET Core SignalR
MessagePack Hub Protocol in SignalR for ASP.NET Core

Additional resources
ASP.NET Core SignalR JavaScript client
Use hubs in ASP.NET Core SignalR
Use ASP.NET Core SignalR with Blazor
Article • 06/08/2023

This tutorial provides a basic working experience for building a real-time app using
SignalR with Blazor. For detailed Blazor guidance, see the Blazor reference
documentation.

Learn how to:

" Create a Blazor project


" Add the SignalR client library
" Add a SignalR hub
" Add SignalR services and an endpoint for the SignalR hub
" Add Razor component code for chat

At the end of this tutorial, you'll have a working chat app.

Prerequisites
Visual Studio

Install either of the following:

Visual Studio 2022 or later with the ASP.NET and web development workload
.NET if it isn't already installed on the system or if the system doesn't have
the latest version installed.

Sample app
Downloading the tutorial's sample chat app isn't required for this tutorial. The sample
app is the final, working app produced by following the steps of this tutorial.

View or download sample code

Create a Blazor Server app


Follow the guidance for your choice of tooling:

Visual Studio
7 Note

Visual Studio 2022 or later and .NET Core SDK 6.0.0 or later are required.

Create a new project.

Select the Blazor Server App template. Select Next.

Type BlazorServerSignalRApp in the Project name field. Confirm the Location entry
is correct or provide a location for the project. Select Next.

Select Create.

Add the SignalR client library


Visual Studio

In Solution Explorer, right-click the BlazorServerSignalRApp project and select


Manage NuGet Packages.

In the Manage NuGet Packages dialog, confirm that the Package source is set to
nuget.org .

With Browse selected, type Microsoft.AspNetCore.SignalR.Client in the search box.

In the search results, select the Microsoft.AspNetCore.SignalR.Client package. Set


the version to match the shared framework of the app. Select Install.

If the Preview Changes dialog appears, select OK.

If the License Acceptance dialog appears, select I Accept if you agree with the
license terms.

Add a SignalR hub


Create a Hubs (plural) folder and add the following ChatHub class ( Hubs/ChatHub.cs ):

C#

using Microsoft.AspNetCore.SignalR;
namespace BlazorServerSignalRApp.Server.Hubs;

public class ChatHub : Hub


{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}

Add services and an endpoint for the SignalR


hub
Open the Program.cs file.

Add the namespaces for Microsoft.AspNetCore.ResponseCompression and the ChatHub


class to the top of the file:

C#

using Microsoft.AspNetCore.ResponseCompression;
using BlazorServerSignalRApp.Server.Hubs;

Add Response Compression Middleware services:

C#

builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});

Use Response Compression Middleware at the top of the processing pipeline's


configuration:

C#

app.UseResponseCompression();

Between the endpoints for mapping the Blazor hub and the client-side fallback, add an
endpoint for the hub immediately after the line app.MapBlazorHub(); :

C#
app.MapHub<ChatHub>("/chathub");

Add Razor component code for chat


Open the Pages/Index.razor file.

Replace the markup with the following code:

razor

@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable

<PageTitle>Index</PageTitle>

<div class="form-group">
<label>
User:
<input @bind="userInput" />
</label>
</div>
<div class="form-group">
<label>
Message:
<input @bind="messageInput" size="50" />
</label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
@foreach (var message in messages)
{
<li>@message</li>
}
</ul>

@code {
private HubConnection? hubConnection;
private List<string> messages = new List<string>();
private string? userInput;
private string? messageInput;

protected override async Task OnInitializedAsync()


{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message)
=>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
InvokeAsync(StateHasChanged);
});

await hubConnection.StartAsync();
}

private async Task Send()


{
if (hubConnection is not null)
{
await hubConnection.SendAsync("SendMessage", userInput,
messageInput);
}
}

public bool IsConnected =>


hubConnection?.State == HubConnectionState.Connected;

public async ValueTask DisposeAsync()


{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
}

7 Note

Disable Response Compression Middleware in the Development environment when


using Hot Reload. For more information, see ASP.NET Core Blazor SignalR
guidance.

Run the app


Follow the guidance for your tooling:

Visual Studio

Press F5 to run the app with debugging or Ctrl + F5 (Windows)/ ⌘ + F5 (macOS)


to run the app without debugging.
Copy the URL from the address bar, open another browser instance or tab, and paste
the URL in the address bar.

Choose either browser, enter a name and message, and select the button to send the
message. The name and message are displayed on both pages instantly:

Quotes: Star Trek VI: The Undiscovered Country ©1991 Paramount

Next steps
In this tutorial, you learned how to:

" Create a Blazor project


" Add the SignalR client library
" Add a SignalR hub
" Add SignalR services and an endpoint for the SignalR hub
" Add Razor component code for chat

To learn more about building Blazor apps, see the Blazor documentation:

ASP.NET Core Blazor

Bearer token authentication with Identity Server, WebSockets, and Server-Sent


Events

Additional resources
Secure a SignalR hub in hosted Blazor WebAssembly apps
Overview of ASP.NET Core SignalR
SignalR cross-origin negotiation for authentication
SignalR configuration
Debug ASP.NET Core Blazor WebAssembly
Threat mitigation guidance for ASP.NET Core Blazor Server
Blazor samples GitHub repository (dotnet/blazor-samples)
Tutorial: Create a gRPC client and server
in ASP.NET Core
Article • 05/09/2023

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.

In this tutorial, you:

" Create a gRPC Server.


" Create a gRPC client.
" Test the gRPC client with the gRPC Greeter service.

Prerequisites
Visual Studio

Visual Studio 2022 with the ASP.NET and web development workload.

Create a gRPC service


Visual Studio
Start Visual Studio 2022 and select Create a new project.
In the Create a new project dialog, search for gRPC . Select ASP.NET Core
gRPC Service and select Next.
In the Configure your new project dialog, enter GrpcGreeter for Project
name. It's important to name the project GrpcGreeter so the namespaces
match when you copy and paste code.
Select Next.
In the Additional information dialog, select .NET 6.0 (Long-term support)
and then select Create.

Run the service

Visual Studio

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog when a project is not yet
configured to use SSL:

Select Yes if you trust the IIS Express SSL certificate.

The following dialog is displayed:


Select Yes if you agree to trust the development certificate.

For information on trusting the Firefox browser, see Firefox


SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

Visual Studio:
Starts Kestrel server.
Launches a browser.
Navigates to http://localhost:port , such as http://localhost:7042 .
port: A randomly assigned port number for the app.
localhost : The standard hostname for the local computer. Localhost

only serves web requests from the local computer.

The logs show the service listening on https://localhost:<port> , where <port> is the
localhost port number randomly assigned when the project is created and set in
Properties/launchSettings.json .

Console

info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
7 Note

The gRPC template is configured to use Transport Layer Security (TLS) . gRPC
clients need to use HTTPS to call the server. The gRPC service localhost port
number is randomly assigned when the project is created and set in the
Properties\launchSettings.json file of the gRPC service project.

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
Unable to start ASP.NET Core gRPC app on macOS.

Examine the project files


GrpcGreeter project files:

Protos/greet.proto : defines the Greeter gRPC and is used to generate the gRPC

server assets. For more information, see Introduction to gRPC.


Services folder: Contains the implementation of the Greeter service.
appSettings.json : Contains configuration data such as the protocol used by

Kestrel. For more information, see Configuration in ASP.NET Core.


Program.cs , which contains:

The entry point for the gRPC service. For more information, see .NET Generic
Host in ASP.NET Core.
Code that configures app behavior. For more information, see App startup.

Create the gRPC client in a .NET console app


Visual Studio

Open a second instance of Visual Studio and select Create a new project.
In the Create a new project dialog, select Console Application, and select
Next.
In the Project name text box, enter GrpcGreeterClient and select Next.
In the Additional information dialog, select .NET 6.0 (Long-term support)
and then select Create.

Add required NuGet packages


The gRPC client project requires the following NuGet packages:

Grpc.Net.Client , which contains the .NET Core client.


Google.Protobuf , which contains protobuf message APIs for C#.
Grpc.Tools , which contain C# tooling support for protobuf files. The tooling
package isn't required at runtime, so the dependency is marked with
PrivateAssets="All" .

Visual Studio

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, run cd GrpcGreeterClient to


change directories to the folder containing the GrpcGreeterClient.csproj files.

Run the following commands:

PowerShell

Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools

Manage NuGet Packages option to install packages


Right-click the project in Solution Explorer > Manage NuGet Packages.
Select the Browse tab.
Enter Grpc.Net.Client in the search box.
Select the Grpc.Net.Client package from the Browse tab and select Install.
Repeat for Google.Protobuf and Grpc.Tools .

Add greet.proto
Create a Protos folder in the gRPC client project.
Copy the Protos\greet.proto file from the gRPC Greeter service to the Protos folder
in the gRPC client project.

Update the namespace inside the greet.proto file to the project's namespace:

JSON

option csharp_namespace = "GrpcGreeterClient";

Edit the GrpcGreeterClient.csproj project file:

Visual Studio

Right-click the project and select Edit Project File.

Add an item group with a <Protobuf> element that refers to the greet.proto file:

XML

<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

Create the Greeter client


Build the client project to create the types in the GrpcGreeterClient namespace.

7 Note

The GrpcGreeterClient types are generated automatically by the build process. The
tooling package Grpc.Tools generates the following files based on the greet.proto
file:

GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs : The

protocol buffer code which populates, serializes and retrieves the request and
response message types.
GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs :

Contains the generated client classes.

For more information on the C# assets automatically generated by Grpc.Tools ,


see gRPC services with C#: Generated C# assets.
Update the gRPC client Program.cs file with the following code.

C#

using System.Threading.Tasks;
using Grpc.Net.Client;
using GrpcGreeterClient;

// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();

In the preceding highlighted code, replace the localhost port number 7042 with
the HTTPS port number specified in Properties/launchSettings.json within the
GrpcGreeter service project.

Program.cs contains the entry point and logic for the gRPC client.

The Greeter client is created by:

Instantiating a GrpcChannel containing the information for creating the connection


to the gRPC service.
Using the GrpcChannel to construct the Greeter client:

C#

// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();

The Greeter client calls the asynchronous SayHello method. The result of the SayHello
call is displayed:

C#

// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();

Test the gRPC client with the gRPC Greeter


service
Visual Studio

In the Greeter service, press Ctrl+F5 to start the server without the debugger.
In the GrpcGreeterClient project, press Ctrl+F5 to start the client without the
debugger.

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:

Console

Greeting: Hello GreeterClient


Press any key to exit...

The gRPC service records the details of the successful call in the logs written to the
command prompt:

Console

info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:<port>
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:
<port>/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

Update the appsettings.Development.json file by adding the following lines:

JSON

"Microsoft.AspNetCore.Hosting": "Information",
"Microsoft.AspNetCore.Routing.EndpointMiddleware": "Information"

7 Note

The code in this article requires the ASP.NET Core HTTPS development certificate to
secure the gRPC service. If the .NET gRPC client fails with the message The remote
certificate is invalid according to the validation procedure. or The SSL

connection could not be established. , the development certificate isn't trusted. To


fix this issue, see Call a gRPC service with an untrusted/invalid certificate.

Next steps
View or download the completed sample code for this tutorial (how to
download).
Overview for gRPC on .NET
gRPC services with C#
Migrate gRPC from C-core to gRPC for .NET
Razor Pages with Entity Framework Core
in ASP.NET Core - Tutorial 1 of 8
Article • 04/11/2023

By Tom Dykstra , Jeremy Likness , and Jon P Smith

This is the first in a series of tutorials that show how to use Entity Framework (EF) Core in
an ASP.NET Core Razor Pages app. The tutorials build a web site for a fictional Contoso
University. The site includes functionality such as student admission, course creation,
and instructor assignments. The tutorial uses the code first approach. For information on
following this tutorial using the database first approach, see this Github issue .

Download or view the completed app. Download instructions.

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 2022 with the ASP.NET and web development workload.

Database engines
The Visual Studio instructions use SQL Server LocalDB, a version of SQL Server
Express that runs only on Windows.

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 .

The sample app


The app built in these tutorials is a basic university web site. Users can view and update
student, course, and instructor information. Here are a few of the screens created in the
tutorial.
The UI style of this site is based on the built-in project templates. The tutorial's focus is
on how to use EF Core with ASP.NET Core, not how to customize the UI.

Optional: Build the sample download


This step is optional. Building the completed app is recommended when you have
problems you can't solve. If you run into a problem you can't resolve, compare your
code to the completed project . Download instructions.

Visual Studio

Select ContosoUniversity.csproj to open the project.

Build the project.

In Package Manager Console (PMC) run the following command:

PowerShell

Update-Database
Run the project to seed the database.

Create the web app project


Visual Studio

1. Start Visual Studio 2022 and select Create a new project.

2. In the Create a new project dialog, select ASP.NET Core Web App, and then
select Next.
3. In the Configure your new project dialog, enter ContosoUniversity for Project
name. It's important to name the project ContosoUniversity, including
matching the capitalization, so the namespaces will match when you copy and
paste example code.

4. Select Next.

5. In the Additional information dialog, select .NET 6.0 (Long-term support)


and then select Create.
Set up the site style
Copy and paste the following code into the Pages/Shared/_Layout.cshtml file:

CSHTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"
/>
<link rel="stylesheet" href="~/ContosoUniversity.styles.css" asp-append-
version="true" />
</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-area="" asp-
page="/Index">Contoso University</a>
<button class="navbar-toggler" type="button" data-bs-
toggle="collapse" data-bs-target=".navbar-collapse" aria-
controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle
navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
page="/About">About</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
page="/Students/Index">Students</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
page="/Courses/Index">Courses</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
page="/Instructors/Index">Instructors</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
page="/Departments/Index">Departments</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">


<div class="container">
&copy; 2021 - Contoso University - <a asp-area="" asp-
page="/Privacy">Privacy</a>
</div>
</footer>

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

@await RenderSectionAsync("Scripts", required: false)


</body>
</html>

The layout file sets the site header, footer, and menu. The preceding code makes the
following changes:

Each occurrence of "ContosoUniversity" to "Contoso University". There are three


occurrences.
The Home and Privacy menu entries are deleted.
Entries are added for About, Students, Courses, Instructors, and Departments.

In Pages/Index.cshtml , replace the contents of the file with the following code:

CSHTML

@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}

<div class="row mb-auto">


<div class="col-md-4">
<div class="row no-gutters border mb-4">
<div class="col p-4 mb-4 ">
<p class="card-text">
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>
</div>
<div class="col-md-4">
<div class="row no-gutters border mb-4">
<div class="col p-4 d-flex flex-column position-static">
<p class="card-text mb-auto">
You can build the application by following the steps in
a series of tutorials.
</p>
<p>
@* <a
href="https://docs.microsoft.com/aspnet/core/data/ef-rp/intro"
class="stretched-link">See the tutorial</a>
*@ </p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row no-gutters border mb-4">
<div class="col p-4 d-flex flex-column">
<p class="card-text mb-auto">
You can download the completed project from GitHub.
</p>
<p>
@* <a
href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef
-rp/intro/samples" class="stretched-link">See project source code</a>
*@ </p>
</div>
</div>
</div>
</div>
The preceding code replaces the text about ASP.NET Core with text about this app.

Run the app to verify that the home page appears.

The data model


The following sections create a data model:

A student can enroll in any number of courses, and a course can have any number of
students enrolled in it.

The Student entity

Create a Models folder in the project folder.


Create Models/Student.cs with the following code:

C#

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

public ICollection<Enrollment> Enrollments { 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 . For more information, see EF Core - Keys.

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. Other collection types can be used, such as
List<Enrollment> or HashSet<Enrollment> . When ICollection<Enrollment> is used, EF
Core creates a HashSet<Enrollment> collection by default.

The Enrollment entity

Create Models/Enrollment.cs with the following code:


C#

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

The EnrollmentID property is the primary key; this entity uses the classnameID pattern
instead of ID by itself. For a production data model, many developers 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 .
The Course entity

Create Models/Course.cs with the following code:

C#

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

public ICollection<Enrollment> Enrollments { 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 app. The compiler generates several warnings about how null values are
handled. See this GitHub issue , Nullable reference types, and Tutorial: Express your
design intent more clearly with nullable and non-nullable reference types for more
information.

To eliminate the warnings from nullable reference types, remove the following line from
the ContosoUniversity.csproj file:

XML

<Nullable>enable</Nullable>
The scaffolding engine currently does not support nullable reference types, therefore
the models used in scaffold can't either.

Remove the ? nullable reference type annotation from public string? RequestId {
get; set; } in Pages/Error.cshtml.cs so the project builds without compiler warnings.

Scaffold Student pages


In this section, the ASP.NET Core scaffolding tool is used to generate:

An EF Core DbContext class. The context is the main class that coordinates Entity
Framework functionality for a given data model. It derives from the
Microsoft.EntityFrameworkCore.DbContext class.
Razor pages that handle Create, Read, Update, and Delete (CRUD) operations for
the Student entity.

Visual Studio

Create a Pages/Students folder.


In Solution Explorer, right-click the Pages/Students folder and select Add >
New Scaffolded Item.
In the Add New Scaffold Item dialog:
In the left tab, select Installed > Common > Razor Pages
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 to end in SchoolContext rather than
ContosoUniversityContext . The updated context name:

ContosoUniversity.Data.SchoolContext

Select Add to finish adding the data context class.


Select Add to finish the Add Razor Pages dialog.

The following packages are automatically installed:

Microsoft.EntityFrameworkCore.SqlServer

Microsoft.EntityFrameworkCore.Tools
Microsoft.VisualStudio.Web.CodeGeneration.Design
If the preceding step fails, 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 Program.cs .
Adds a database connection string to appsettings.json .

Database connection string


The scaffolding tool generates a connection string in the appsettings.json file.

Visual Studio

The connection string specifies SQL Server LocalDB:

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SchoolContext": "Server=
(localdb)\\mssqllocaldb;Database=SchoolContext-
0e9;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.
Update the database context class
The main class that coordinates EF Core functionality for a given data model is the
database context class. The context is derived from
Microsoft.EntityFrameworkCore.DbContext. The context specifies which entities are
included in the data model. In this project, the class is named SchoolContext .

Update Data/SchoolContext.cs with the following code:

C#

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext (DbContextOptions<SchoolContext> options)
: base(options)
{
}

public DbSet<Student> Students { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Course> Courses { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}

The preceding code changes from the singular DbSet<Student> Student to the plural
DbSet<Student> Students . To make the Razor Pages code match the new DBSet name,
make a global change from: _context.Student. to: _context.Students.

There are 8 occurrences.

Because an entity set contains multiple entities, many developers prefer the DBSet
property names should be plural.

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.
Calls OnModelCreating. OnModelCreating :
Is called when SchoolContext has been initialized, but before the model has
been locked down and used to initialize the context.
Is required because later in the tutorial the Student entity will have references
to the other entities.

We hope to fix this issue in a future release.

Program.cs
ASP.NET Core is built with dependency injection. Services such as the SchoolContext are
registered with dependency injection during app 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

The following highlighted lines were added by the scaffolder:

C#

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolCo
ntext")));

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 or the
appsettings.Development.json file.
Add the database exception filter
Add AddDatabaseDeveloperPageExceptionFilter and UseMigrationsEndPoint as shown
in the following code:

Visual Studio

C#

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolCo
ntext")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
else
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}

Add the Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet package.

In the Package Manager Console, enter the following to add the NuGet package:

PowerShell

Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore

The Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet package provides


ASP.NET Core middleware for Entity Framework Core error pages. This middleware helps
to detect and diagnose errors with Entity Framework Core migrations.
The AddDatabaseDeveloperPageExceptionFilter provides helpful error information in the
development environment for EF migrations errors.

Create the database


Update Program.cs to create the database if it doesn't exist:

Visual Studio

C#

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolCo
ntext")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
else
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}

using (var scope = app.Services.CreateScope())


{
var services = scope.ServiceProvider;

var context = services.GetRequiredService<SchoolContext>();


context.Database.EnsureCreated();
// DbInitializer.Initialize(context);
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

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 early in development when the schema is rapidly evolving, as long
as data doesn't need to be preserved. 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, the database is deleted that was created by EnsureCreated
and migrations is used. 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.

Seed the database


The EnsureCreated method creates an empty database. This section adds code that
populates the database with test data.

Create Data/DbInitializer.cs with the following code:

C#

using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}

var students = new Student[]


{
new
Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.P
arse("2019-09-01")},
new
Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Pa
rse("2017-09-01")},
new
Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse
("2018-09-01")},
new
Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Pa
rse("2017-09-01")},
new
Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2017
-09-01")},
new
Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Pars
e("2016-09-01")},
new
Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse
("2018-09-01")},
new
Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Pars
e("2019-09-01")}
};

context.Students.AddRange(students);
context.SaveChanges();

var courses = new Course[]


{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};

context.Courses.AddRange(courses);
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};

context.Enrollments.AddRange(enrollments);
context.SaveChanges();
}
}
}

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 , remove // from the DbInitializer.Initialize line:

C#

using (var scope = app.Services.CreateScope())


{
var services = scope.ServiceProvider;

var context = services.GetRequiredService<SchoolContext>();


context.Database.EnsureCreated();
DbInitializer.Initialize(context);
}

Visual Studio

Stop the app if it's running, and run the following command in the Package
Manager Console (PMC):

PowerShell

Drop-Database -Confirm
Respond with Y to delete the database.

Restart the app.


Select the Students page to see the seeded data.

View the database


Visual Studio

Open SQL Server Object Explorer (SSOX) from the View menu in Visual
Studio.
In SSOX, select (localdb)\MSSQLLocalDB > Databases > SchoolContext-
{GUID}. The database name is generated from the context name provided
earlier plus a dash and a 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.
Right-click the Student table and click View Code to see how the Student
model maps to the Student table schema.

Asynchronous EF methods in ASP.NET Core web


apps
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 doing 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 return value, await keyword, and
ToListAsync method make the code execute asynchronously.

C#

public async Task OnGetAsync()


{
Students = await _context.Students.ToListAsync();
}

The async keyword tells the compiler to:


Generate callbacks for parts of the method body.
Create the Task object that's returned.
The Task return type represents ongoing work.
The await keyword causes the compiler to split the method into two parts. The
first part ends with the operation that's started asynchronously. The second part is
put into a callback method that's called when the operation completes.
ToListAsync is the asynchronous version of the ToList extension method.

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.

2 Warning

The async implementation of Microsoft.Data.SqlClient has some known issues


(#593 , #601 , and others). If you're seeing unexpected performance problems,
try using sync command execution instead, especially when dealing with large text
or binary values.
Performance considerations
In general, a web page shouldn't be loading an arbitrary number of rows. A query
should use paging or a limiting approach. For example, the preceding query could use
Take to limit the rows returned:

C#

public async Task OnGetAsync()


{
Student = await _context.Students.Take(10).ToListAsync();
}

Enumerating a large table in a view could return a partially constructed HTTP 200
response if a database exception occurs part way through the enumeration.

Paging is covered later in the tutorial.

For more information, see Performance considerations (EF).

Next steps
Use SQLite for development, SQL Server for production

Next tutorial
Part 2, Razor Pages with EF Core in
ASP.NET Core - CRUD
Article • 04/11/2023

By Tom Dykstra , Jeremy Likness , and Jon P Smith

The Contoso University web app demonstrates how to create Razor Pages web apps
using EF Core and Visual Studio. For information about the tutorial series, see the first
tutorial.

If you run into problems you can't solve, download the completed app and compare
that code to what you created by following the tutorial.

In this tutorial, the scaffolded CRUD (create, read, update, delete) code is reviewed and
customized.

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.

Update the Details page


The scaffolded code for the Students pages doesn't include enrollment data. In this
section, enrollments are added to the Details page.

Read enrollments
To display a student's enrollment data on the page, the enrollment data must be read.
The scaffolded code in Pages/Students/Details.cshtml.cs reads only the Student data,
without the Enrollment data:

C#

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

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.

C#

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Student = await _context.Students


.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

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

Read 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.

CSHTML

@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.

Route data vs. query string


The URL for the Details page is https://localhost:<port>/Students/Details?id=1 . The
entity's primary key value is in the query string. Some developers prefer to pass the key
value in route data: https://localhost:<port>/Students/Details/1 . For more
information, see Update the generated code.

Update the Create page


The scaffolded OnPostAsync code for the Create page is vulnerable to overposting.
Replace the OnPostAsync method in Pages/Students/Create.cshtml.cs with the
following code.

C#

public async Task<IActionResult> OnPostAsync()


{
var emptyStudent = new Student();

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 is 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:

C#

public class Student


{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}

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 page, 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 StudentVM view model:

C#

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:

C#

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

var entry = _context.Add(new Student());


entry.CurrentValues.SetValues(StudentVM);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

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.


Needs to have properties that match.

Using StudentVM requires the Create page use StudentVM rather than Student :

CSHTML

@page
@model CreateVMModel

@{
ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Student</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="StudentVM.LastName" class="control-label">
</label>
<input asp-for="StudentVM.LastName" class="form-control" />
<span asp-validation-for="StudentVM.LastName" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="StudentVM.FirstMidName" class="control-
label"></label>
<input asp-for="StudentVM.FirstMidName" class="form-control"
/>
<span asp-validation-for="StudentVM.FirstMidName"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StudentVM.EnrollmentDate" class="control-
label"></label>
<input asp-for="StudentVM.EnrollmentDate" class="form-
control" />
<span asp-validation-for="StudentVM.EnrollmentDate"
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");}
}

Update the Edit page


In Pages/Students/Edit.cshtml.cs , replace the OnGetAsync and OnPostAsync methods
with the following code.

C#

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}
Student = await _context.Students.FindAsync(id);

if (Student == null)
{
return NotFound();
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
var studentToUpdate = await _context.Students.FindAsync(id);

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.

Modified : Some or all of the entity's property values have been modified. The

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.

Update the Delete page


In this section, a custom error message is implemented when the call to SaveChanges
fails.

Replace the code in Pages/Students/Delete.cshtml.cs with the following code:

C#

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
private readonly ILogger<DeleteModel> _logger;

public DeleteModel(ContosoUniversity.Data.SchoolContext context,


ILogger<DeleteModel> logger)
{
_context = context;
_logger = logger;
}

[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }

public async Task<IActionResult> OnGetAsync(int? id, bool?


saveChangesError = false)
{
if (id == null)
{
return NotFound();
}

Student = await _context.Students


.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

if (Student == null)
{
return NotFound();
}

if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = String.Format("Delete {ID} failed. Try
again", id);
}

return Page();
}

public async Task<IActionResult> OnPostAsync(int? id)


{
if (id == null)
{
return NotFound();
}

var student = await _context.Students.FindAsync(id);

if (student == null)
{
return NotFound();
}
try
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, ErrorMessage);

return RedirectToAction("./Delete",
new { id, saveChangesError = true });
}
}
}
}

The preceding code:

Adds Logging.
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 OnPostAsync method 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 database exception is caught.


The Delete pages OnGetAsync method is called with saveChangesError=true .

Add an error message to Pages/Students/Delete.cshtml :

CSHTML

@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>


<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>
</dl>

<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
Previous tutorial Next tutorial
Part 3, Razor Pages with EF Core in
ASP.NET Core - Sort, Filter, Paging
Article • 04/11/2023

By Tom Dykstra , Jeremy Likness , and Jon P Smith

The Contoso University web app demonstrates how to create Razor Pages web apps
using EF Core and Visual Studio. For information about the tutorial series, see the first
tutorial.

If you run into problems you can't solve, download the completed app and compare
that code to what you created by following the tutorial.

This tutorial adds sorting, filtering, and paging functionality to the Students pages.

The following illustration shows a completed page. The column headings are clickable
links to sort the column. Click a column heading repeatedly to switch between
ascending and descending sort order.
Add sorting
Replace the code in Pages/Students/Index.cshtml.cs with the following code to add
sorting.

C#

public class IndexModel : PageModel


{
private readonly SchoolContext _context;
public IndexModel(SchoolContext context)
{
_context = context;
}

public string NameSort { get; set; }


public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }

public IList<Student> Students { get; set; }

public async Task OnGetAsync(string sortOrder)


{
// using System;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

IQueryable<Student> studentsIQ = from s in _context.Students


select s;

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

Students = await studentsIQ.AsNoTracking().ToListAsync();


}
}
The preceding code:

Requires adding using System; .


Adds properties to contain the sorting parameters.
Changes the name of the Student property to Students .
Replaces the code in the OnGetAsync method.

The OnGetAsync method receives a sortOrder parameter from the query string in the
URL. The URL and 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 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:

C#

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";


DateSort = sortOrder == "Date" ? "date_desc" : "Date";

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:

Current sort order Last Name Hyperlink Date Hyperlink

Last Name ascending descending ascending

Last Name descending ascending ascending

Date ascending ascending descending

Date descending ascending ascending

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:

C#

IQueryable<Student> studentsIQ = from s in _context.Students


select s;

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

Students = await studentsIQ.AsNoTracking().ToListAsync();

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:

C#

Students = await studentsIQ.AsNoTracking().ToListAsync();

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.

CSHTML
@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 .

To verify that sorting works:

Run the app and select the Students tab.


Click the column headings.

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:

C#

public class IndexModel : PageModel


{
private readonly SchoolContext _context;

public IndexModel(SchoolContext context)


{
_context = context;
}

public string NameSort { get; set; }


public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }
public IList<Student> Students { get; set; }

public async Task OnGetAsync(string sortOrder, string searchString)


{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

CurrentFilter = searchString;

IQueryable<Student> studentsIQ = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
studentsIQ = studentsIQ.Where(s =>
s.LastName.Contains(searchString)
||
s.FirstMidName.Contains(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;
}

Students = await studentsIQ.AsNoTracking().ToListAsync();


}
}

The preceding code:

Adds the searchString parameter to the OnGetAsync method, and saves the
parameter value in the CurrentFilter property. The search string value is received
from a text box that's added in the next section.
Adds to the LINQ statement a Where clause. The Where clause selects only students
whose first name or last name contains the search string. The LINQ statement is
executed only if there's a value to search for.

IQueryable vs. IEnumerable


The code calls the Where method on an IQueryable object, and the filter is processed
on the server. In some scenarios, the app might be calling the Where method as an
extension method on an in-memory collection. For example, suppose
_context.Students changes from EF Core DbSet to a repository method that returns an
IEnumerable collection. The result would normally be the same but in some cases may

be different.

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:

C#

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`

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 add a Search button.

CSHTML
@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">


<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString"
value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-primary" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>

<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:

browser-address-bar

https://localhost:5001/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.

Create the PaginatedList class


In the project folder, create PaginatedList.cs with the following code:

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(List<T> items, int count, int pageIndex, int
pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);

this.AddRange(items);
}

public bool HasPreviousPage => PageIndex > 1;

public bool HasNextPage => PageIndex < TotalPages;

public static async Task<PaginatedList<T>> CreateAsync(


IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip(
(pageIndex - 1) * pageSize)
.Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}

The CreateAsync method 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 page size to configuration


Add PageSize to the appsettings.json Configuration file:

JSON

{
"PageSize": 3,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Add paging to IndexModel


Replace the code in Students/Index.cshtml.cs to add paging.

C#

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
private readonly IConfiguration Configuration;

public IndexModel(SchoolContext context, IConfiguration


configuration)
{
_context = context;
Configuration = configuration;
}

public string NameSort { get; set; }


public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }
public PaginatedList<Student> Students { get; set; }

public async Task OnGetAsync(string sortOrder,


string currentFilter, string searchString, int? pageIndex)
{
CurrentSort = sortOrder;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}

CurrentFilter = searchString;

IQueryable<Student> studentsIQ = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
studentsIQ = studentsIQ.Where(s =>
s.LastName.Contains(searchString)
||
s.FirstMidName.Contains(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;
}

var pageSize = Configuration.GetValue("PageSize", 4);


Students = await PaginatedList<Student>.CreateAsync(
studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
}
}

The preceding code:


Changes the type of the Students property from IList<Student> to
PaginatedList<Student> .
Adds the page index, the current sortOrder , and the currentFilter to the
OnGetAsync method signature.
Saves the sort order in the CurrentSort property.
Resets page index to 1 when there's a new search string.
Uses the PaginatedList class to get Student entities.
Sets pageSize to 3 from Configuration, 4 if configuration fails.

All the parameters that OnGetAsync receives are null when:

The page is called from the Students link.


The user hasn't clicked a paging or sorting link.

When a paging link is clicked, the page index variable contains the page number to
display.

The CurrentSort property provides the Razor Page with the current sort order. The
current sort order must be included in the paging links to keep the sort order while
paging.

The CurrentFilter property provides the Razor Page with the current filter string. The
CurrentFilter value:

Must be included in the paging links in order to maintain the filter settings during
paging.
Must be restored to the text box when the page is redisplayed.

If the search string is changed while paging, the page is reset to 1. The page has to be
reset to 1 because the new filter can result in different data to display. When a search
value is entered and Submit is selected:

The search string is changed.


The searchString parameter isn't null.

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 returns the value of pageIndex if it has a
value, otherwise, it returns 1.
Add paging links
Replace the code in Students/Index.cshtml with the following code. The changes are
highlighted:

CSHTML

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
<a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">


<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString"
value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-primary" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>

<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:

CSHTML
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>

The paging buttons are displayed by tag helpers:

CSHTML

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

Run the app and navigate to the students page.

To make sure paging works, click the paging links in different sort orders.
To verify that paging works correctly with sorting and filtering, enter a search string
and try paging.
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:

C#

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }

public int StudentCount { get; set; }


}
}

Create the Razor Page


Create a Pages/About.cshtml file with the following code:

CSHTML

@page
@model ContosoUniversity.Pages.AboutModel

@{
ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>

@foreach (var item in Model.Students)


{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>

Create the page model


Update the Pages/About.cshtml.cs file with the following code:

C#

using ContosoUniversity.Models.SchoolViewModels;
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;

public AboutModel(SchoolContext context)


{
_context = context;
}

public IList<EnrollmentDateGroup> Students { get; set; }

public async Task OnGetAsync()


{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};

Students = await data.AsNoTracking().ToListAsync();


}
}
}

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.

Previous tutorial Next tutorial


Part 4, Razor Pages with EF Core
migrations in ASP.NET Core
Article • 04/11/2023

By Tom Dykstra , Jon P Smith , and Rick Anderson

The Contoso University web app demonstrates how to create Razor Pages web apps
using EF Core and Visual Studio. For information about the tutorial series, see the first
tutorial.

If you run into problems you can't solve, download the completed app and compare
that code to what you created by following the tutorial.

This tutorial introduces the EF Core migrations feature for managing data model
changes.

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 series started by
configuring the Entity Framework to create the database if it doesn't exist. Each time the
data model changes, the database needs to be dropped. The next time the app runs, the
call to EnsureCreated re-creates the database to match the new data model. The
DbInitializer class then runs to seed the new database.

This approach to keeping the DB in sync with the data model works well until the app
needs to be deployed 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
database.

Rather than dropping and recreating the database when the data model changes,
migrations updates the schema and retains existing data.

7 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 goes in the
Up and Down methods for a migration and involves:

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

Use SQL Server Object Explorer (SSOX) to delete the database, or run the following
command in the Package Manager Console (PMC):

PowerShell

Drop-Database

Create an initial migration


Visual Studio

Run the following commands in the PMC:


PowerShell

Add-Migration InitialCreate
Update-Database

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 Program.cs , delete the following line:

C#

context.Database.EnsureCreated();

Run the app and verify that the database is seeded.

Up and Down methods


The EF Core migrations add command generated code to create the database. This
migrations code is in the Migrations\<timestamp>_InitialCreate.cs file. The Up method
of the InitialCreate class creates the database tables that correspond to the data
model entity sets. The Down method deletes them, as shown in the following example:

C#

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

protected override void Down(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropTable(
name: "Enrollment");

migrationBuilder.DropTable(
name: "Course");

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

The preceding code is for the initial migration. The code:

Was generated by the migrations add InitialCreate command.


Is executed by the database update command.
Creates a database for the data model specified by the database context class.

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."

The migrations history table


Use SSOX or SQLite tool to inspect the database.
Notice the addition of an __EFMigrationsHistory table. The __EFMigrationsHistory
table keeps track of which migrations have been applied to the database.
View the data in the __EFMigrationsHistory table. It shows one row for the first
migration.

The data model snapshot


Migrations creates a snapshot of the current data model in
Migrations/SchoolContextModelSnapshot.cs . When add a migration is added, EF
determines what changed by comparing the current data model to the snapshot file.

Because the snapshot file tracks the state of the data model, a migration cannot be
deleted by deleting the <timestamp>_<migrationname>.cs file. To back out the most
recent migration, use the migrations remove command. migrations remove deletes the
migration and ensures the snapshot is correctly reset. For more information, see dotnet
ef migrations remove.

See Resetting all migrations to remove all migrations.

Applying migrations in production


We recommend that production apps not call Database.Migrate at application startup.
Migrate shouldn't be called from an app that is deployed to a server farm. If the app is

scaled out to multiple server instances, it's hard to ensure database schema updates
don't happen from multiple servers or conflict with read/write access.

Database migration should be done as part of deployment, and in a controlled way.


Production database migration approaches include:

Using migrations to create SQL scripts and using the SQL scripts in deployment.
Running dotnet ef database update from a controlled environment.

Troubleshooting
If the app uses SQL Server LocalDB and displays the following exception:

text

SqlException: Cannot open database "ContosoUniversity" requested by the


login.
The login failed.
Login failed for user 'user name'.

The solution may be to run dotnet ef database update at a command prompt.

Additional resources
EF Core CLI.
dotnet ef migrations CLI commands
Package Manager Console (Visual Studio)

Next steps
The next tutorial builds out the data model, adding entity properties and new entities.

Previous tutorial Next tutorial


Part 5, Razor Pages with EF Core in
ASP.NET Core - Data Model
Article • 04/11/2023

By Tom Dykstra , Jeremy Likness , and Jon P Smith

The Contoso University web app demonstrates how to create Razor Pages web apps
using EF Core and Visual Studio. For information about the tutorial series, see the first
tutorial.

If you run into problems you can't solve, download the completed app and compare
that code to what you created by following the tutorial.

The previous tutorials worked with a basic data model that was composed of three
entities. In this tutorial:

More entities and relationships are added.


The data model is customized by specifying formatting, validation, and database
mapping rules.

The completed data model is shown in the following illustration:


The following database diagram was made with Dataedo :
To create a database diagram with Dataedo:

Deploy the app to Azure


Download and install Dataedo on your computer.
Follow the instructions Generate documentation for Azure SQL Database in 5
minutes

In the preceding Dataedo diagram, the CourseInstructor is a join table created by Entity
Framework. For more information, see Many-to-many

The Student entity


Replace the code in Models/Student.cs with the following code:

C#

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

public ICollection<Enrollment> Enrollments { get; set; }


}
}

The preceding code adds a FullName property and adds the following attributes to
existing properties:

[DataType]
[DisplayFormat]
[StringLength]
[Column]
[Required]
[Display]

The FullName calculated property


FullName is a calculated property that returns a value that's created by concatenating

two other properties. FullName can't be set, so it has only a get accessor. No FullName
column is created in the database.

The DataType attribute


C#

[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


C#

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =


true)]

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


C#

[StringLength(50, ErrorMessage = "First name cannot be longer than 50


characters.")]

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:

C#

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

Visual Studio

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


C#

[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
C#

[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 must be used with MinimumLength for the MinimumLength to be
enforced.

C#

[Display(Name = "Last Name")]


[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLength and Required allow whitespace to satisfy the validation. Use the
RegularExpression attribute for full control over the string.

The Display attribute


C#

[Display(Name = "Last Name")]

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
The error message is similar to the following example:

SqlException: Invalid column name 'FirstName'.


There are pending model changes
Pending model changes are detected in the following:

SchoolContext

In the PMC, enter the following commands to create a new migration and
update the database:

PowerShell

Add-Migration ColumnFirstName
Update-Database

The first of these commands generates the following warning message:

text

An operation was scaffolded that may result in the loss of data.


Please review the migration for accuracy.

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 .

Run the app and go to the Students page.


Notice that times are not input or displayed along with dates.
Select Create New, and try to enter a name longer than 50 characters.

7 Note

In the following sections, building the app at some stages generates compiler
errors. The instructions specify when to build the app.

The Instructor Entity


Create Models/Instructor.cs with the following code:

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

[Display(Name = "Full Name")]


public string FullName
{
get { return LastName + ", " + FirstMidName; }
}

public ICollection<Course> Courses { get; set; }


public OfficeAssignment OfficeAssignment { get; set; }
}
}

Multiple attributes can be on one line. The HireDate attributes could be written as
follows:

C#

[DataType(DataType.Date),Display(Name = "Hire
Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]

Navigation properties
The Courses and OfficeAssignment properties are navigation properties.

An instructor can teach any number of courses, so Courses is defined as a collection.

C#
public ICollection<Course> Courses { get; set; }

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.

C#

public OfficeAssignment OfficeAssignment { get; set; }

The OfficeAssignment entity

Create Models/OfficeAssignment.cs with the following code:

C#

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

public Instructor Instructor { get; set; }


}
}

The Key attribute


The [Key] attribute is used to identify a property as the primary key (PK) when the
property name is something other than classnameID or ID .
There's a one-to-zero-or-one relationship between the Instructor and
OfficeAssignment entities. An office assignment only exists in relation to the instructor
it's assigned to. The OfficeAssignment PK is also its foreign key (FK) to the Instructor
entity. A one-to-zero-or-one relationship occurs when a PK in one table is both a PK and
a FK in another table.

EF Core can't automatically recognize InstructorID as the PK of OfficeAssignment


because InstructorID doesn't follow the ID or classnameID naming convention.
Therefore, the Key attribute is used to identify InstructorID as the PK:

C#

[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. For more information, see EF Keys.

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.

The Course Entity


Update Models/Course.cs with the following code:

C#

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

[StringLength(50, MinimumLength = 3)]


public string Title { get; set; }

[Range(0, 5)]
public int Credits { get; set; }

public int DepartmentID { get; set; }

public Department Department { get; set; }


public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<Instructor> Instructors { 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.

C#

[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.

C#

public int DepartmentID { get; set; }


public Department Department { get; set; }

A course can have any number of students enrolled in it, so the Enrollments navigation
property is a collection:

C#

public ICollection<Enrollment> Enrollments { get; set; }

A course may be taught by multiple instructors, so the Instructors navigation property


is a collection:

C#

public ICollection<Instructor> Instructors { get; set; }

The Department entity


Create Models/Department.cs with the following code:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { 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; }

public int? InstructorID { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}

The Column attribute


Previously the Column attribute was used to change column name mapping. In the code
for the Department entity, the Column attribute is used to change SQL data type
mapping. The Budget column is defined using the SQL Server money type in the
database:

C#

[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 navigation property is named Administrator but holds an Instructor entity:

C#

public int? InstructorID { get; set; }


public Instructor Administrator { get; set; }

The ? in the preceding code specifies the property is nullable.

A department may have many courses, so there's a Courses navigation property:

C#

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.

C#

modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)

The Enrollment foreign key and navigation properties


An enrollment record is for one course taken by one student.
Update Models/Enrollment.cs with the following code:

C#

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

The FK properties and navigation properties reflect the following relationships:

An enrollment record is for one course, so there's a CourseID FK property and a Course
navigation property:

C#

public int CourseID { get; set; }


public Course Course { get; set; }

An enrollment record is for one student, so there's a StudentID FK property and a


Student navigation property:
C#

public int StudentID { get; set; }


public Student Student { get; set; }

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 the Enrollment entity, the additional data besides FKs are 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 Enrollment table didn't include grade information, it would only need to contain
the two FKs, CourseID and 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 PJT.

Update the database context


Update Data/SchoolContext.cs with the following code:

C#

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable(nameof(Course))
.HasMany(c => c.Instructors)
.WithMany(i => i.Courses);
modelBuilder.Entity<Student>().ToTable(nameof(Student));
modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
}
}
}

The preceding code adds the new entities and configures the many-to-many
relationship between the Instructor and Course entities.

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:

C#

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}

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, for example, 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:

Choose one of these two approaches.


Use the chosen approach consistently as much as possible.

Some of the attributes used in this tutorial are used for:

Validation only (for example, MinimumLength ).


EF Core configuration only (for example, HasKey ).
Validation and EF Core configuration (for example, [StringLength(50)] ).

For more information about attributes vs. fluent API, see Methods of configuration.

Seed the database


Update the code in Data/DbInitializer.cs :

C#
using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}

var alexander = new Student


{
FirstMidName = "Carson",
LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2016-09-01")
};

var alonso = new Student


{
FirstMidName = "Meredith",
LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2018-09-01")
};

var anand = new Student


{
FirstMidName = "Arturo",
LastName = "Anand",
EnrollmentDate = DateTime.Parse("2019-09-01")
};

var barzdukas = new Student


{
FirstMidName = "Gytis",
LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2018-09-01")
};

var li = new Student


{
FirstMidName = "Yan",
LastName = "Li",
EnrollmentDate = DateTime.Parse("2018-09-01")
};

var justice = new Student


{
FirstMidName = "Peggy",
LastName = "Justice",
EnrollmentDate = DateTime.Parse("2017-09-01")
};

var norman = new Student


{
FirstMidName = "Laura",
LastName = "Norman",
EnrollmentDate = DateTime.Parse("2019-09-01")
};

var olivetto = new Student


{
FirstMidName = "Nino",
LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2011-09-01")
};

var students = new Student[]


{
alexander,
alonso,
anand,
barzdukas,
li,
justice,
norman,
olivetto
};

context.AddRange(students);

var abercrombie = new Instructor


{
FirstMidName = "Kim",
LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11")
};

var fakhouri = new Instructor


{
FirstMidName = "Fadi",
LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06")
};

var harui = new Instructor


{
FirstMidName = "Roger",
LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01")
};

var kapoor = new Instructor


{
FirstMidName = "Candace",
LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15")
};

var zheng = new Instructor


{
FirstMidName = "Roger",
LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12")
};

var instructors = new Instructor[]


{
abercrombie,
fakhouri,
harui,
kapoor,
zheng
};

context.AddRange(instructors);

var officeAssignments = new OfficeAssignment[]


{
new OfficeAssignment {
Instructor = fakhouri,
Location = "Smith 17" },
new OfficeAssignment {
Instructor = harui,
Location = "Gowan 27" },
new OfficeAssignment {
Instructor = kapoor,
Location = "Thompson 304" }
};

context.AddRange(officeAssignments);

var english = new Department


{
Name = "English",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = abercrombie
};

var mathematics = new Department


{
Name = "Mathematics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = fakhouri
};
var engineering = new Department
{
Name = "Engineering",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = harui
};

var economics = new Department


{
Name = "Economics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = kapoor
};

var departments = new Department[]


{
english,
mathematics,
engineering,
economics
};

context.AddRange(departments);

var chemistry = new Course


{
CourseID = 1050,
Title = "Chemistry",
Credits = 3,
Department = engineering,
Instructors = new List<Instructor> { kapoor, harui }
};

var microeconomics = new Course


{
CourseID = 4022,
Title = "Microeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};

var macroeconmics = new Course


{
CourseID = 4041,
Title = "Macroeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};

var calculus = new Course


{
CourseID = 1045,
Title = "Calculus",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { fakhouri }
};

var trigonometry = new Course


{
CourseID = 3141,
Title = "Trigonometry",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { harui }
};

var composition = new Course


{
CourseID = 2021,
Title = "Composition",
Credits = 3,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};

var literature = new Course


{
CourseID = 2042,
Title = "Literature",
Credits = 4,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};

var courses = new Course[]


{
chemistry,
microeconomics,
macroeconmics,
calculus,
trigonometry,
composition,
literature
};

context.AddRange(courses);

var enrollments = new Enrollment[]


{
new Enrollment {
Student = alexander,
Course = chemistry,
Grade = Grade.A
},
new Enrollment {
Student = alexander,
Course = microeconomics,
Grade = Grade.C
},
new Enrollment {
Student = alexander,
Course = macroeconmics,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = calculus,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = trigonometry,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = anand,
Course = chemistry
},
new Enrollment {
Student = anand,
Course = microeconomics,
Grade = Grade.B
},
new Enrollment {
Student = barzdukas,
Course = chemistry,
Grade = Grade.B
},
new Enrollment {
Student = li,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = justice,
Course = literature,
Grade = Grade.B
}
};

context.AddRange(enrollments);
context.SaveChanges();
}
}
}
The preceding code provides seed data for the new entities. Most of this code creates
new entity objects and loads sample data. The sample data is used for testing.

Apply the migration or drop and re-create


With the existing database, there are two approaches to changing the database:

Drop and re-create the database. Choose this section when using SQLite.
Apply the migration to the existing database. The instructions in this section work
for SQL Server only, not for SQLite.

Either choice works for SQL Server. While the apply-migration method is more complex
and time-consuming, it's the preferred approach for real-world, production
environments.

Drop and re-create the database


To force EF Core to create a new database, drop and update the database:

Visual Studio

Delete the Migrations folder.


In the Package Manager Console (PMC), run the following commands:

PowerShell

Drop-Database
Add-Migration InitialCreate
Update-Database

Run the app. Running the app runs the DbInitializer.Initialize method. The
DbInitializer.Initialize populates the new database.

Visual Studio

Open the database in SSOX:

If SSOX was opened previously, click the Refresh button.


Expand the Tables node. The created tables are displayed.
Next steps
The next two tutorials show how to read and update related data.

Previous tutorial Next tutorial


Part 6, Razor Pages with EF Core in
ASP.NET Core - Read Related Data
Article • 03/28/2023

By Tom Dykstra , Jon P Smith , and Rick Anderson

The Contoso University web app demonstrates how to create Razor Pages web apps
using EF Core and Visual Studio. For information about the tutorial series, see the first
tutorial.

If you run into problems you can't solve, download the completed app and compare
that code to what you created by following the tutorial.

This tutorial shows how to read and display related data. Related data is data that EF
Core loads into navigation properties.

The following illustrations show the completed pages for this tutorial:
Eager, explicit, and lazy loading
There are several ways that EF Core can load related data into the navigation properties
of an entity:

Eager loading. Eager loading is when a query for one type of entity also loads
related entities. When an entity is read, its related data is retrieved. This typically
results in a single join query that retrieves all of the data that's needed. EF Core will
issue multiple queries for some types of eager loading. Issuing multiple queries
can be more efficient than a large single query. Eager loading is specified with the
Include and ThenInclude methods.
Eager loading sends multiple queries when a collection navigation is included:
One query for the main query
One query for each collection "edge" in the load tree.

Separate queries with Load : The data can be retrieved in separate queries, and EF
Core "fixes up" the navigation properties. "Fixes up" means that EF Core
automatically populates the navigation properties. Separate queries with Load is
more like explicit loading than eager loading.

Note: EF Core automatically fixes up navigation properties to any other entities


that were previously loaded into the context instance. Even if the data for a
navigation property is not explicitly included, the property may still be populated if
some or all of the related entities were previously loaded.

Explicit loading. When the entity is first read, related data isn't retrieved. Code
must be written to retrieve the related data when it's needed. Explicit loading with
separate queries results in multiple queries sent to the database. With explicit
loading, the code specifies the navigation properties to be loaded. Use the Load
method to do explicit loading. For example:

Lazy loading. When the entity is first read, related data isn't retrieved. The first time
a navigation property is accessed, the data required for that navigation property is
automatically retrieved. A query is sent to the database each time a navigation
property is accessed for the first time. Lazy loading can hurt performance, for
example when developers use N+1 queries . N+1 queries load a parent and
enumerate through children.

Create Course pages


The Course entity includes a navigation property that contains the related Department
entity.

To display the name of the assigned department for a course:

Load the related Department entity into the Course.Department navigation


property.
Get the name from the Department entity's Name property.

Scaffold Course pages

Visual Studio
Follow the instructions in Scaffold Student pages with the following
exceptions:
Create a Pages/Courses folder.
Use Course for the model class.
Use the existing context class instead of creating a new one.

Open Pages/Courses/Index.cshtml.cs and examine the OnGetAsync method. The


scaffolding engine specified eager loading for the Department navigation property.
The Include method specifies eager loading.

Run the app and select the Courses link. The department column displays the
DepartmentID , which isn't useful.

Display the department name


Update Pages/Courses/Index.cshtml.cs with the following code:

C#

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public IndexModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

public IList<Course> Courses { get; set; }

public async Task OnGetAsync()


{
Courses = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
}
}
The preceding code changes the Course property to Courses and adds AsNoTracking .

No-tracking queries are useful when the results are used in a read-only scenario. They're
generally quicker to execute because there's no need to set up the change tracking
information. If the entities retrieved from the database don't need to be updated, then a
no-tracking query is likely to perform better than a tracking query.

In some cases a tracking query is more efficient than a no-tracking query. For more
information, see Tracking vs. No-Tracking Queries. In the preceding code, AsNoTracking
is called because the entities aren't updated in the current context.

Update Pages/Courses/Index.cshtml with the following code.

CSHTML

@page
@model ContosoUniversity.Pages.Courses.IndexModel

@{
ViewData["Title"] = "Courses";
}

<h1>Courses</h1>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Courses[0].CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Courses)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a>
|
<a asp-page="./Details" asp-route-
id="@item.CourseID">Details</a> |
<a asp-page="./Delete" asp-route-
id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

The following changes have been made to the scaffolded code:

Changed the Course property name to Courses .

Added a Number column that shows the CourseID property value. By default,
primary keys aren't scaffolded because normally they're meaningless to end users.
However, in this case the primary key is meaningful.

Changed the Department column to display the department name. The code
displays the Name property of the Department entity that's loaded into the
Department navigation property:

HTML

@Html.DisplayFor(modelItem => item.Department.Name)

Run the app and select the Courses tab to see the list with department names.
Loading related data with Select
The OnGetAsync method loads related data with the Include method. The Select
method is an alternative that loads only the related data needed. For single items, like
the Department.Name it uses a SQL INNER JOIN . For collections, it uses another database
access, but so does the Include operator on collections.

The following code loads related data with the Select method:

C#

public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()


{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}

The preceding code doesn't return any entity types, therefore no tracking is done. For
more information about the EF tracking, see Tracking vs. No-Tracking Queries.

The CourseViewModel :

C#
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}

See IndexSelectModel for the complete Razor Pages.

Create Instructor pages


This section scaffolds Instructor pages and adds related Courses and Enrollments to the
Instructors Index page.
This page reads and displays related data in the following ways:

The list of instructors displays related data from the OfficeAssignment entity
(Office in the preceding image). The Instructor and OfficeAssignment entities are
in a one-to-zero-or-one relationship. Eager loading is used for the
OfficeAssignment entities. Eager loading is typically more efficient when the

related data needs to be displayed. In this case, office assignments for the
instructors are displayed.
When the user selects an instructor, related Course entities are displayed. The
Instructor and Course entities are in a many-to-many relationship. Eager loading

is used for the Course entities and their related Department entities. In this case,
separate queries might be more efficient because only courses for the selected
instructor are needed. This example shows how to use eager loading for navigation
properties in entities that are in navigation properties.
When the user selects a course, related data from the Enrollments entity is
displayed. In the preceding image, student name and grade are displayed. The
Course and Enrollment entities are in a one-to-many relationship.

Create a view model


The instructors page shows data from three different tables. A view model is needed
that includes three properties representing the three tables.

Create Models/SchoolViewModels/InstructorIndexData.cs with the following code:

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}

Scaffold Instructor pages

Visual Studio

Follow the instructions in Scaffold the student pages with the following
exceptions:
Create a Pages/Instructors folder.
Use Instructor for the model class.
Use the existing context class instead of creating a new one.

Run the app and navigate to the Instructors page.


Update Pages/Instructors/Index.cshtml.cs with the following code:

C#

using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels; // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public IndexModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

public InstructorIndexData InstructorData { get; set; }


public int InstructorID { get; set; }
public int CourseID { get; set; }

public async Task OnGetAsync(int? id, int? courseID)


{
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.ThenInclude(c => c.Department)
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.Courses;
}

if (courseID != null)
{
CourseID = courseID.Value;
IEnumerable<Enrollment> Enrollments = await
_context.Enrollments
.Where(x => x.CourseID == CourseID)
.Include(i=>i.Student)
.ToListAsync();
InstructorData.Enrollments = Enrollments;
}
}
}
}

The OnGetAsync method accepts optional route data for the ID of the selected instructor.

Examine the query in the Pages/Instructors/Index.cshtml.cs file:

C#

InstructorData = new InstructorIndexData();


InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.ThenInclude(c => c.Department)
.OrderBy(i => i.LastName)
.ToListAsync();

The code specifies eager loading for the following navigation properties:

Instructor.OfficeAssignment
Instructor.Courses

Course.Department

The following code executes when an instructor is selected, that is, id != null .

C#

if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.Courses;
}

The selected instructor is retrieved from the list of instructors in the view model. The
view model's Courses property is loaded with the Course entities from the selected
instructor's Courses navigation property.

The Where method returns a collection. In this case, the filter select a single entity, so the
Single method is called to convert the collection into a single Instructor entity. The

Instructor entity provides access to the Course navigation property.


The Single method is used on a collection when the collection has only one item. The
Single method throws an exception if the collection is empty or if there's more than
one item. An alternative is SingleOrDefault, which returns a default value if the collection
is empty. For this query, null in the default returned.

The following code populates the view model's Enrollments property when a course is
selected:

C#

if (courseID != null)
{
CourseID = courseID.Value;
IEnumerable<Enrollment> Enrollments = await _context.Enrollments
.Where(x => x.CourseID == CourseID)
.Include(i=>i.Student)
.ToListAsync();
InstructorData.Enrollments = Enrollments;
}

Update the instructors Index page


Update Pages/Instructors/Index.cshtml with the following code.

CSHTML

@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel

@{
ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.InstructorData.Instructors)
{
string selectedRow = "";
if (item.ID == Model.InstructorID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@{
foreach (var course in item.Courses)
{
@course.CourseID @: @course.Title <br />
}
}
</td>
<td>
<a asp-page="./Index" asp-route-id="@item.ID">Select</a>
|
<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>

@if (Model.InstructorData.Courses != null)


{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>

@foreach (var item in Model.InstructorData.Courses)


{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
<a asp-page="./Index" asp-route-
courseID="@item.CourseID">Select</a>
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}

</table>
}

@if (Model.InstructorData.Enrollments != null)


{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.InstructorData.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}

The preceding code makes the following changes:


Updates the page directive to @page "{id:int?}" . "{id:int?}" is a route template.
The route template changes integer query strings in the URL to route data. For
example, clicking on the Select link for an instructor with only the @page directive
produces a URL like the following:

https://localhost:5001/Instructors?id=2

When the page directive is @page "{id:int?}" , the URL is:


https://localhost:5001/Instructors/2

Adds an Office column that displays item.OfficeAssignment.Location only if


item.OfficeAssignment isn't null. Because this is a one-to-zero-or-one relationship,
there might not be a related OfficeAssignment entity.

HTML

@if (item.OfficeAssignment != null)


{
@item.OfficeAssignment.Location
}

Adds a Courses column that displays courses taught by each instructor. See Explicit
line transition for more about this razor syntax.

Adds code that dynamically adds class="table-success" to the tr element of the


selected instructor and course. This sets a background color for the selected row
using a Bootstrap class.

HTML

string selectedRow = "";


if (item.CourseID == Model.CourseID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">

Adds a new hyperlink labeled Select. This link sends the selected instructor's ID to
the Index method and sets a background color.

HTML

<a asp-action="Index" asp-route-id="@item.ID">Select</a> |

Adds a table of courses for the selected Instructor.


Adds a table of student enrollments for the selected course.

Run the app and select the Instructors tab. The page displays the Location (office) from
the related OfficeAssignment entity. If OfficeAssignment is null, an empty table cell is
displayed.

Click on the Select link for an instructor. The row style changes and courses assigned to
that instructor are displayed.

Select a course to see the list of enrolled students and their grades.

Next steps
The next tutorial shows how to update related data.

Previous tutorial Next tutorial


Part 7, Razor Pages with EF Core in
ASP.NET Core - Update Related Data
Article • 04/11/2023

By Tom Dykstra , Jon P Smith , and Rick Anderson

The Contoso University web app demonstrates how to create Razor Pages web apps
using EF Core and Visual Studio. For information about the tutorial series, see the first
tutorial.

If you run into problems you can't solve, download the completed app and compare
that code to what you created by following the tutorial.

This tutorial shows how to update related data. The following illustrations show some of
the completed pages.
Update the Course Create and Edit pages
The scaffolded code for the Course Create and Edit pages has a Department drop-down
list that shows DepartmentID , an int . The drop-down should show the Department
name, so both of these pages need a list of department names. To provide that list, use
a base class for the Create and Edit pages.

Create a base class for Course Create and Edit


Create a Pages/Courses/DepartmentNamePageModel.cs file with the following code:

C#

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace ContosoUniversity.Pages.Courses
{
public class DepartmentNamePageModel : PageModel
{
public SelectList DepartmentNameSL { get; set; }

public void PopulateDepartmentsDropDownList(SchoolContext _context,


object selectedDepartment = null)
{
var departmentsQuery = from d in _context.Departments
orderby d.Name // Sort by name.
select d;

DepartmentNameSL = new
SelectList(departmentsQuery.AsNoTracking(),
nameof(Department.DepartmentID),
nameof(Department.Name),
selectedDepartment);
}
}
}
The preceding code creates a SelectList to contain the list of department names. If
selectedDepartment is specified, that department is selected in the SelectList .

The Create and Edit page model classes will derive from DepartmentNamePageModel .

Update the Course Create page model


A Course is assigned to a Department. The base class for the Create and Edit pages
provides a SelectList for selecting the department. The drop-down list that uses the
SelectList sets the Course.DepartmentID foreign key (FK) property. EF Core uses the
Course.DepartmentID FK to load the Department navigation property.

Update Pages/Courses/Create.cshtml.cs with the following code:

C#
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
public class CreateModel : DepartmentNamePageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public CreateModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

public IActionResult OnGet()


{
PopulateDepartmentsDropDownList(_context);
return Page();
}

[BindProperty]
public Course Course { get; set; }

public async Task<IActionResult> OnPostAsync()


{
var emptyCourse = new Course();

if (await TryUpdateModelAsync<Course>(
emptyCourse,
"course", // Prefix for form value.
s => s.CourseID, s => s.DepartmentID, s => s.Title, s =>
s.Credits))
{
_context.Courses.Add(emptyCourse);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

// Select DepartmentID if TryUpdateModelAsync fails.


PopulateDepartmentsDropDownList(_context,
emptyCourse.DepartmentID);
return Page();
}
}
}

If you would like to see code comments translated to languages other than English, let
us know in this GitHub discussion issue .

The preceding code:


Derives from DepartmentNamePageModel .
Uses TryUpdateModelAsync to prevent overposting.
Removes ViewData["DepartmentID"] . The DepartmentNameSL SelectList is a
strongly typed model and will be used by the Razor page. Strongly typed models
are preferred over weakly typed. For more information, see Weakly typed data
(ViewData and ViewBag).

Update the Course Create Razor page


Update Pages/Courses/Create.cshtml with the following code:

CSHTML

@page
@model ContosoUniversity.Pages.Courses.CreateModel
@{
ViewData["Title"] = "Create Course";
}
<h2>Create</h2>
<h4>Course</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="Course.CourseID" class="control-label">
</label>
<input asp-for="Course.CourseID" class="form-control" />
<span asp-validation-for="Course.CourseID" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Title" class="control-label"></label>
<input asp-for="Course.Title" class="form-control" />
<span asp-validation-for="Course.Title" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="Course.Credits" class="control-label">
</label>
<input asp-for="Course.Credits" class="form-control" />
<span asp-validation-for="Course.Credits" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Department" class="control-label">
</label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="Course.DepartmentID" class="text-
danger" />
</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");}
}

The preceding code makes the following changes:

Changes the caption from DepartmentID to Department.


Replaces "ViewBag.DepartmentID" with DepartmentNameSL (from the base class).
Adds the "Select Department" option. This change renders "Select Department" in
the drop-down when no department has been selected yet, rather than the first
department.
Adds a validation message when the department isn't selected.

The Razor Page uses the Select Tag Helper:

CSHTML

<div class="form-group">
<label asp-for="Course.Department" class="control-label"></label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="Course.DepartmentID" class="text-danger" />
</div>

Test the Create page. The Create page displays the department name rather than the
department ID.
Update the Course Edit page model
Update Pages/Courses/Edit.cshtml.cs with the following code:

C#

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
public class EditModel : DepartmentNamePageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Course Course { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Course = await _context.Courses


.Include(c => c.Department).FirstOrDefaultAsync(m =>
m.CourseID == id);

if (Course == null)
{
return NotFound();
}

// Select current DepartmentID.


PopulateDepartmentsDropDownList(_context, Course.DepartmentID);
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}

var courseToUpdate = await _context.Courses.FindAsync(id);

if (courseToUpdate == null)
{
return NotFound();
}

if (await TryUpdateModelAsync<Course>(
courseToUpdate,
"course", // Prefix for form value.
c => c.Credits, c => c.DepartmentID, c => c.Title))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}

// Select DepartmentID if TryUpdateModelAsync fails.


PopulateDepartmentsDropDownList(_context,
courseToUpdate.DepartmentID);
return Page();
}
}
}

The changes are similar to those made in the Create page model. In the preceding code,
PopulateDepartmentsDropDownList passes in the department ID, which selects that

department in the drop-down list.

Update the Course Edit Razor page


Update Pages/Courses/Edit.cshtml with the following code:

CSHTML

@page
@model ContosoUniversity.Pages.Courses.EditModel

@{
ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<h4>Course</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<input type="hidden" asp-for="Course.CourseID" />
<div class="form-group">
<label asp-for="Course.CourseID" class="control-label">
</label>
<div>@Html.DisplayFor(model => model.Course.CourseID)</div>
</div>
<div class="form-group">
<label asp-for="Course.Title" class="control-label"></label>
<input asp-for="Course.Title" class="form-control" />
<span asp-validation-for="Course.Title" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="Course.Credits" class="control-label">
</label>
<input asp-for="Course.Credits" class="form-control" />
<span asp-validation-for="Course.Credits" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Department" class="control-label">
</label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL"></select>
<span asp-validation-for="Course.DepartmentID" 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-page="./Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

The preceding code makes the following changes:

Displays the course ID. Generally the Primary Key (PK) of an entity isn't displayed.
PKs are usually meaningless to users. In this case, the PK is the course number.
Changes the caption for the Department drop-down from DepartmentID to
Department.
Replaces "ViewBag.DepartmentID" with DepartmentNameSL , which is in the base class.

The page contains a hidden field ( <input type="hidden"> ) for the course number.
Adding a <label> tag helper with asp-for="Course.CourseID" doesn't eliminate the
need for the hidden field. <input type="hidden"> is required for the course number to
be included in the posted data when the user selects Save.

Update the Course page models


AsNoTracking can improve performance when tracking isn't required.

Update Pages/Courses/Delete.cshtml.cs and Pages/Courses/Details.cshtml.cs by


adding AsNoTracking to the OnGetAsync methods:

C#

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Course = await _context.Courses


.AsNoTracking()
.Include(c => c.Department)
.FirstOrDefaultAsync(m => m.CourseID == id);

if (Course == null)
{
return NotFound();
}
return Page();
}

Update the Course Razor pages


Update Pages/Courses/Delete.cshtml with the following code:

CSHTML

@page
@model ContosoUniversity.Pages.Courses.DeleteModel
@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Course</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.CourseID)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Department)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Department.Name)
</dd>
</dl>

<form method="post">
<input type="hidden" asp-for="Course.CourseID" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>

Make the same changes to the Details page.

CSHTML

@page
@model ContosoUniversity.Pages.Courses.DetailsModel

@{
ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
<h4>Course</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.CourseID)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Department)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Department.Name)
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Course.CourseID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>

Test the Course pages


Test the create, edit, details, and delete pages.

Update the instructor Create and Edit pages


Instructors may teach any number of courses. The following image shows the instructor
Edit page with an array of course checkboxes.
The checkboxes enable changes to courses an instructor is assigned to. A checkbox is
displayed for every course in the database. Courses that the instructor is assigned to are
selected. The user can select or clear checkboxes to change course assignments. If the
number of courses were much greater, a different UI might work better. But the method
of managing a many-to-many relationship shown here wouldn't change. To create or
delete relationships, you manipulate a join entity.

Create a class for assigned courses data


Create Models/SchoolViewModels/AssignedCourseData.cs with the following code:

C#

namespace ContosoUniversity.Models.SchoolViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}

The AssignedCourseData class contains data to create the checkboxes for courses
assigned to an instructor.

Create an Instructor page model base class


Create the Pages/Instructors/InstructorCoursesPageModel.cs base class:

C#

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Pages.Instructors
{
public class InstructorCoursesPageModel : PageModel
{
public List<AssignedCourseData> AssignedCourseDataList;

public void PopulateAssignedCourseData(SchoolContext context,


Instructor instructor)
{
var allCourses = context.Courses;
var instructorCourses = new HashSet<int>(
instructor.Courses.Select(c => c.CourseID));
AssignedCourseDataList = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
AssignedCourseDataList.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
}
}
}
The InstructorCoursesPageModel is the base class for the Edit and Create page models.
PopulateAssignedCourseData reads all Course entities to populate
AssignedCourseDataList . For each course, the code sets the CourseID , title, and whether

or not the instructor is assigned to the course. A HashSet is used for efficient lookups.

Handle office location


Another relationship the edit page has to handle is the one-to-zero-or-one relationship
that the Instructor entity has with the OfficeAssignment entity. The instructor edit code
must handle the following scenarios:

If the user clears the office assignment, delete the OfficeAssignment entity.
If the user enters an office assignment and it was empty, create a new
OfficeAssignment entity.

If the user changes the office assignment, update the OfficeAssignment entity.

Update the Instructor Edit page model


Update Pages/Instructors/Edit.cshtml.cs with the following code:

C#

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
public class EditModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Instructor Instructor { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

if (Instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(_context, Instructor);
return Page();
}

public async Task<IActionResult> OnPostAsync(int? id, string[]


selectedCourses)
{
if (id == null)
{
return NotFound();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.FirstOrDefaultAsync(s => s.ID == id);

if (instructorToUpdate == null)
{
return NotFound();
}

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(
instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses,
instructorToUpdate);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
PopulateAssignedCourseData(_context, instructorToUpdate);
return Page();
}

public void UpdateInstructorCourses(string[] selectedCourses,


Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.Courses.Select(c => c.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
var courseToRemove =
instructorToUpdate.Courses.Single(
c => c.CourseID ==
course.CourseID);
instructorToUpdate.Courses.Remove(courseToRemove);
}
}
}
}
}
}

The preceding code:

Gets the current Instructor entity from the database using eager loading for the
OfficeAssignment and Courses navigation properties.
Updates the retrieved Instructor entity with values from the model binder.
TryUpdateModelAsync prevents overposting.
If the office location is blank, sets Instructor.OfficeAssignment to null. When
Instructor.OfficeAssignment is null, the related row in the OfficeAssignment table

is deleted.
Calls PopulateAssignedCourseData in OnGetAsync to provide information for the
checkboxes using the AssignedCourseData view model class.
Calls UpdateInstructorCourses in OnPostAsync to apply information from the
checkboxes to the Instructor entity being edited.
Calls PopulateAssignedCourseData and UpdateInstructorCourses in OnPostAsync if
TryUpdateModelAsync fails. These method calls restore the assigned course data
entered on the page when it is redisplayed with an error message.

Since the Razor page doesn't have a collection of Course entities, the model binder can't
automatically update the Courses navigation property. Instead of using the model
binder to update the Courses navigation property, that's done in the new
UpdateInstructorCourses method. Therefore you need to exclude the Courses property
from model binding. This doesn't require any change to the code that calls
TryUpdateModelAsync because you're using the overload with declared properties and
Courses isn't in the include list.

If no checkboxes were selected, the code in UpdateInstructorCourses initializes the


instructorToUpdate.Courses with an empty collection and returns:

C#

if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}

The code then loops through all courses in the database and checks each course against
the ones currently assigned to the instructor versus the ones that were selected in the
page. To facilitate efficient lookups, the latter two collections are stored in HashSet
objects.

If the checkbox for a course is selected but the course is not in the Instructor.Courses
navigation property, the course is added to the collection in the navigation property.

C#

if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
If the checkbox for a course is not selected, but the course is in the Instructor.Courses
navigation property, the course is removed from the navigation property.

C#

else
{
if (instructorCourses.Contains(course.CourseID))
{
var courseToRemove = instructorToUpdate.Courses.Single(
c => c.CourseID == course.CourseID);
instructorToUpdate.Courses.Remove(courseToRemove);
}
}

Update the Instructor Edit Razor page


Update Pages/Instructors/Edit.cshtml with the following code:

CSHTML

@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<input type="hidden" asp-for="Instructor.ID" />
<div class="form-group">
<label asp-for="Instructor.LastName" class="control-label">
</label>
<input asp-for="Instructor.LastName" class="form-control" />
<span asp-validation-for="Instructor.LastName" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.FirstMidName" class="control-
label"></label>
<input asp-for="Instructor.FirstMidName" class="form-
control" />
<span asp-validation-for="Instructor.FirstMidName"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.HireDate" class="control-label">
</label>
<input asp-for="Instructor.HireDate" class="form-control" />
<span asp-validation-for="Instructor.HireDate" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.OfficeAssignment.Location"
class="control-label"></label>
<input asp-for="Instructor.OfficeAssignment.Location"
class="form-control" />
<span asp-validation-
for="Instructor.OfficeAssignment.Location" class="text-danger" />
</div>
<div class="form-group">
<div class="table">
<table>
<tr>
@{
int cnt = 0;

foreach (var course in


Model.AssignedCourseDataList)
{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ?
"checked=\"checked\"" : "")) />
@course.CourseID @:
@course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>

<div>
<a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

The preceding code creates an HTML table that has three columns. Each column has a
checkbox and a caption containing the course number and title. The checkboxes all have
the same name ("selectedCourses"). Using the same name informs the model binder to
treat them as a group. The value attribute of each checkbox is set to CourseID . When
the page is posted, the model binder passes an array that consists of the CourseID
values for only the checkboxes that are selected.

When the checkboxes are initially rendered, courses assigned to the instructor are
selected.

Note: The approach taken here to edit instructor course data works well when there's a
limited number of courses. For collections that are much larger, a different UI and a
different updating method would be more useable and efficient.

Run the app and test the updated Instructors Edit page. Change some course
assignments. The changes are reflected on the Index page.

Update the Instructor Create page


Update the Instructor Create page model and with code similar to the Edit page:

C#

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
public class CreateModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
private readonly ILogger<InstructorCoursesPageModel> _logger;

public CreateModel(SchoolContext context,


ILogger<InstructorCoursesPageModel> logger)
{
_context = context;
_logger = logger;
}
public IActionResult OnGet()
{
var instructor = new Instructor();
instructor.Courses = new List<Course>();

// Provides an empty collection for the foreach loop


// foreach (var course in Model.AssignedCourseDataList)
// in the Create Razor page.
PopulateAssignedCourseData(_context, instructor);
return Page();
}

[BindProperty]
public Instructor Instructor { get; set; }

public async Task<IActionResult> OnPostAsync(string[]


selectedCourses)
{
var newInstructor = new Instructor();

if (selectedCourses.Length > 0)
{
newInstructor.Courses = new List<Course>();
// Load collection with one DB call.
_context.Courses.Load();
}

// Add selected Courses courses to the new instructor.


foreach (var course in selectedCourses)
{
var foundCourse = await
_context.Courses.FindAsync(int.Parse(course));
if (foundCourse != null)
{
newInstructor.Courses.Add(foundCourse);
}
else
{
_logger.LogWarning("Course {course} not found", course);
}
}

try
{
if (await TryUpdateModelAsync<Instructor>(
newInstructor,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
_context.Instructors.Add(newInstructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return RedirectToPage("./Index");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}

PopulateAssignedCourseData(_context, newInstructor);
return Page();
}
}
}

The preceding code:

Adds logging for warning and error messages.

Calls Load, which fetches all the Courses in one database call. For small collections
this is an optimization when using FindAsync. FindAsync returns the tracked entity
without a request to the database.

C#

public async Task<IActionResult> OnPostAsync(string[] selectedCourses)


{
var newInstructor = new Instructor();

if (selectedCourses.Length > 0)
{
newInstructor.Courses = new List<Course>();
// Load collection with one DB call.
_context.Courses.Load();
}

// Add selected Courses courses to the new instructor.


foreach (var course in selectedCourses)
{
var foundCourse = await
_context.Courses.FindAsync(int.Parse(course));
if (foundCourse != null)
{
newInstructor.Courses.Add(foundCourse);
}
else
{
_logger.LogWarning("Course {course} not found", course);
}
}

try
{
if (await TryUpdateModelAsync<Instructor>(
newInstructor,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
_context.Instructors.Add(newInstructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return RedirectToPage("./Index");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}

PopulateAssignedCourseData(_context, newInstructor);
return Page();
}

_context.Instructors.Add(newInstructor) creates a new Instructor using many-


to-many relationships without explicitly mapping the join table. Many-to-many
was added in EF 5.0.

Test the instructor Create page.

Update the Instructor Create Razor page with code similar to the Edit page:

CSHTML

@page
@model ContosoUniversity.Pages.Instructors.CreateModel

@{
ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Instructor</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="Instructor.LastName" class="control-label">
</label>
<input asp-for="Instructor.LastName" class="form-control" />
<span asp-validation-for="Instructor.LastName" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.FirstMidName" class="control-
label"></label>
<input asp-for="Instructor.FirstMidName" class="form-
control" />
<span asp-validation-for="Instructor.FirstMidName"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.HireDate" class="control-label">
</label>
<input asp-for="Instructor.HireDate" class="form-control" />
<span asp-validation-for="Instructor.HireDate" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.OfficeAssignment.Location"
class="control-label"></label>
<input asp-for="Instructor.OfficeAssignment.Location"
class="form-control" />
<span asp-validation-
for="Instructor.OfficeAssignment.Location" class="text-danger" />
</div>
<div class="form-group">
<div class="table">
<table>
<tr>
@{
int cnt = 0;

foreach (var course in


Model.AssignedCourseDataList)
{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ?
"checked=\"checked\"" : "")) />
@course.CourseID @:
@course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</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");}
}

Update the Instructor Delete page


Update Pages/Instructors/Delete.cshtml.cs with the following code:

C#

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public DeleteModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Instructor Instructor { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Instructor = await _context.Instructors.FirstOrDefaultAsync(m =>


m.ID == id);
if (Instructor == null)
{
return NotFound();
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Instructor instructor = await _context.Instructors


.Include(i => i.Courses)
.SingleAsync(i => i.ID == id);

if (instructor == null)
{
return RedirectToPage("./Index");
}

var departments = await _context.Departments


.Where(d => d.InstructorID == id)
.ToListAsync();
departments.ForEach(d => d.InstructorID = null);

_context.Instructors.Remove(instructor);

await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}

The preceding code makes the following changes:

Uses eager loading for the Courses navigation property. Courses must be included
or they aren't deleted when the instructor is deleted. To avoid needing to read
them, configure cascade delete in the database.

If the instructor to be deleted is assigned as administrator of any departments,


removes the instructor assignment from those departments.

Run the app and test the Delete page.

Next steps
Previous tutorial Next tutorial
Part 8, Razor Pages with EF Core in
ASP.NET Core - Concurrency
Article • 04/11/2023

Tom Dykstra , and Jon P Smith

The Contoso University web app demonstrates how to create Razor Pages web apps
using EF Core and Visual Studio. For information about the tutorial series, see the first
tutorial.

If you run into problems you can't solve, download the completed app and compare
that code to what you created by following the tutorial.

This tutorial shows how to handle conflicts when multiple users update an entity
concurrently.

Concurrency conflicts
A concurrency conflict occurs when:

A user navigates to the edit page for an entity.


Another user updates the same entity before the first user's change is written to
the database.

If concurrency detection isn't enabled, whoever updates the database last overwrites the
other user's changes. If this risk is acceptable, the cost of programming for concurrency
might outweigh the benefit.

Pessimistic concurrency
One way to prevent concurrency conflicts is to use database locks. This is called
pessimistic concurrency. Before the app reads a database row that it intends to update,
it requests a lock. Once a row is locked for update access, no other users are allowed to
lock the row until the first lock is released.

Managing locks has disadvantages. It can be complex to program and can cause
performance problems as the number of users increases. Entity Framework Core
provides no built-in support for pessimistic concurrency.

Optimistic concurrency
Optimistic concurrency allows concurrency conflicts to happen, and then reacts
appropriately when they do. For example, Jane visits the Department edit page and
changes the budget for the English department from $350,000.00 to $0.00.

Before Jane clicks Save, John visits the same page and changes the Start Date field from
9/1/2007 to 9/1/2013.
Jane clicks Save first and sees her change take effect, since the browser displays the
Index page with zero as the Budget amount.

John clicks Save on an Edit page that still shows a budget of $350,000.00. What happens
next is determined by how you handle concurrency conflicts:

Keep track of which property a user has modified and update only the
corresponding columns in the database.

In the scenario, no data would be lost. Different properties were updated by the
two users. The next time someone browses the English department, they will see
both Jane's and John's changes. This method of updating can reduce the number
of conflicts that could result in data loss. This approach has some disadvantages:
Can't avoid data loss if competing changes are made to the same property.
Is generally not practical in a web app. It requires maintaining significant state in
order to keep track of all fetched values and new values. Maintaining large
amounts of state can affect app performance.
Can increase app complexity compared to concurrency detection on an entity.

Let John's change overwrite Jane's change.

The next time someone browses the English department, they will see 9/1/2013
and the fetched $350,000.00 value. This approach is called a Client Wins or Last in
Wins scenario. All values from the client take precedence over what's in the data
store. The scaffolded code does no concurrency handling, Client Wins happens
automatically.

Prevent John's change from being updated in the database. Typically, the app
would:
Display an error message.
Show the current state of the data.
Allow the user to reapply the changes.

This is called a Store Wins scenario. The data-store values take precedence over the
values submitted by the client. The Store Wins scenario is used in this tutorial. This
method ensures that no changes are overwritten without a user being alerted.

Conflict detection in EF Core


Properties configured as concurrency tokens are used to implement optimistic
concurrency control. When an update or delete operation is triggered by SaveChanges
or SaveChangesAsync, the value of the concurrency token in the database is compared
against the original value read by EF Core:

If the values match, the operation can complete.


If the values do not match, EF Core assumes that another user has performed a
conflicting operation, aborts the current transaction, and throws a
DbUpdateConcurrencyException.

Another user or process performing an operation that conflicts with the current
operation is known as concurrency conflict.

On relational databases EF Core checks for the value of the concurrency token in the
WHERE clause of UPDATE and DELETE statements to detect a concurrency conflict.

The data model must be configured to enable conflict detection by including a tracking
column that can be used to determine when a row has been changed. EF provides two
approaches for concurrency tokens:
Applying [ConcurrencyCheck] or IsConcurrencyToken to a property on the model.
This approach is not recommended. For more information, see Concurrency Tokens
in EF Core.

Applying TimestampAttribute or IsRowVersion to a concurrency token in the


model. This is the approach used in this tutorial.

The SQL Server approach and SQLite implementation details are slightly different. A
difference file is shown later in the tutorial listing the differences. The Visual Studio tab
shows the SQL Server approach. The Visual Studio Code tab shows the approach for
non-SQL Server databases, such as SQLite.

Visual Studio

In the model, include a tracking column that is used to determine when a row
has been changed.
Apply the TimestampAttribute to the concurrency property.

Update the Models/Department.cs file with the following highlighted code:

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { 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; }

public int? InstructorID { get; set; }

[Timestamp]
public byte[] ConcurrencyToken { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}

The TimestampAttribute is what identifies the column as a concurrency tracking


column. The fluent API is an alternative way to specify the tracking property:

C#

modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();

The [Timestamp] attribute on an entity property generates the following code in the
ModelBuilder method:

C#

b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");

The preceding code:

Sets the property type ConcurrencyToken to byte array. byte[] is the required
type for SQL Server.
Calls IsConcurrencyToken. IsConcurrencyToken configures the property as a
concurrency token. On updates, the concurrency token value in the database
is compared to the original value to ensure it has not changed since the
instance was retrieved from the database. If it has changed, a
DbUpdateConcurrencyException is thrown and changes are not applied.
Calls ValueGeneratedOnAddOrUpdate, which configures the ConcurrencyToken
property to have a value automatically generated when adding or updating an
entity.
HasColumnType("rowversion") sets the column type in the SQL Server database
to rowversion.

The following code shows a portion of the T-SQL generated by EF Core when the
Department name is updated:
SQL

SET NOCOUNT ON;


UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

The preceding highlighted code shows the WHERE clause containing


ConcurrencyToken . If the database ConcurrencyToken doesn't equal the

ConcurrencyToken parameter @p2 , no rows are updated.

The following highlighted code shows the T-SQL that verifies exactly one row was
updated:

SQL

SET NOCOUNT ON;


UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

@@ROWCOUNT returns the number of rows affected by the last statement. If no


rows are updated, EF Core throws a DbUpdateConcurrencyException .

Add a migration
Adding the ConcurrencyToken property changes the data model, which requires a
migration.

Build the project.

Visual Studio

Run the following commands in the PMC:

PowerShell

Add-Migration RowVersion
Update-Database
The preceding commands:

Creates the Migrations/{time stamp}_RowVersion.cs migration file.


Updates the Migrations/SchoolContextModelSnapshot.cs file. The update adds
the following code to the BuildModel method:

C#

b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");

Scaffold Department pages


Visual Studio

Follow the instructions in Scaffold Student pages with the following exceptions:

Create a Pages/Departments folder.


Use Department for the model class.
Use the existing context class instead of creating a new one.

Add a utility class


In the project folder, create the Utility class with the following code:

Visual Studio

C#

namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
The Utility class provides the GetLastChars method used to display the last few
characters of the concurrency token. The following code shows the code that works with
both SQLite ad SQL Server:

C#

#if SQLiteVersion
using System;

namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(Guid token)
{
return token.ToString().Substring(
token.ToString().Length - 3);
}
}
}
#else
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
#endif

The #if SQLiteVersion preprocessor directive isolates the differences in the SQLite and
SQL Server versions and helps:

The author maintain one code base for both versions.


SQLite developers deploy the app to Azure and use SQL Azure.

Build the project.

Update the Index page


The scaffolding tool created a ConcurrencyToken column for the Index page, but that
field wouldn't be displayed in a production app. In this tutorial, the last portion of the
ConcurrencyToken is displayed to help show how concurrency handling works. The last

portion isn't guaranteed to be unique by itself.


Update Pages\Departments\Index.cshtml page:

Replace Index with Departments.


Change the code containing ConcurrencyToken to show just the last few characters.
Replace FirstMidName with FullName .

The following code shows the updated page:

CSHTML

@page
@model ContosoUniversity.Pages.Departments.IndexModel

@{
ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model =>
model.Department[0].Administrator)
</th>
<th>
Token
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem =>
item.Administrator.FullName)
</td>
<td>
@Utility.GetLastChars(item.ConcurrencyToken)
</td>
<td>
<a asp-page="./Edit" asp-route-
id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-
id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-
id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

Update the Edit page model


Update Pages/Departments/Edit.cshtml.cs with the following code:

Visual Studio

C#

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }

public async Task<IActionResult> OnGetAsync(int id)


{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);

if (Department == null)
{
return NotFound();
}

// Use strongly typed data rather than ViewData.


InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");

return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
if (!ModelState.IsValid)
{
return Page();
}

// Fetch current department from DB.


// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);

if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}

// Set ConcurrencyToken to value read in OnGetAsync


_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue =
Department.ConcurrencyToken;

if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s =>
s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues =
(Department)exceptionEntry.Entity;
var databaseEntry =
exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable
to save. " +
"The department was deleted by another
user.");
return Page();
}

var dbValues = (Department)databaseEntry.ToObject();


await SetDbErrorMessage(dbValues, clientValues,
_context);

// Save the current ConcurrencyToken so next


postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken =
(byte[])dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.
{nameof(Department.ConcurrencyToken)}");
}
}

InstructorNameSL = new SelectList(_context.Instructors,


"ID", "FullName", departmentToUpdate.InstructorID);

return Page();
}

private IActionResult HandleDeletedDepartment()


{
// ModelState contains the posted data because of the
deletion error
// and overides the Department instance values when
displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another
user.");
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", Department.InstructorID);
return Page();
}
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{

if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}

ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in
the database "
+ "have been displayed. If you still want to edit this
record, click "
+ "the Save button again.");
}
}
}

The concurrency updates


OriginalValue is updated with the ConcurrencyToken value from the entity when it was
fetched in the OnGetAsync method. EF Core generates a SQL UPDATE command with a
WHERE clause containing the original ConcurrencyToken value. If no rows are affected by

the UPDATE command, a DbUpdateConcurrencyException exception is thrown. No rows are


affected by the UPDATE command when no rows have the original ConcurrencyToken
value.
Visual Studio

C#

public async Task<IActionResult> OnPostAsync(int id)


{
if (!ModelState.IsValid)
{
return Page();
}

// Fetch current department from DB.


// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);

if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}

// Set ConcurrencyToken to value read in OnGetAsync


_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue =
Department.ConcurrencyToken;

In the preceding highlighted code:

The value in Department.ConcurrencyToken is the value when the entity was fetched
in the Get request for the Edit page. The value is provided to the OnPost method
by a hidden field in the Razor page that displays the entity to be edited. The
hidden field value is copied to Department.ConcurrencyToken by the model binder.
OriginalValue is what EF Core uses in the WHERE clause. Before the highlighted line

of code executes:
OriginalValue has the value that was in the database when

FirstOrDefaultAsync was called in this method.

This value might be different from what was displayed on the Edit page.
The highlighted code makes sure that EF Core uses the original ConcurrencyToken
value from the displayed Department entity in the SQL UPDATE statement's WHERE
clause.

The following code shows the Department model. Department is initialized in the:

OnGetAsync method by the EF query.


OnPostAsync method by the hidden field in the Razor page using model binding:
Visual Studio

C#

public class EditModel : PageModel


{
private readonly ContosoUniversity.Data.SchoolContext _context;

public EditModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }

public async Task<IActionResult> OnGetAsync(int id)


{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);

if (Department == null)
{
return NotFound();
}

// Use strongly typed data rather than ViewData.


InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");

return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
if (!ModelState.IsValid)
{
return Page();
}

// Fetch current department from DB.


// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);

if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}

// Set ConcurrencyToken to value read in OnGetAsync


_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue =
Department.ConcurrencyToken;

The preceding code shows the ConcurrencyToken value of the Department entity from
the HTTP POST request is set to the ConcurrencyToken value from the HTTP GET request.

When a concurrency error happens, the following highlighted code gets the client
values (the values posted to this method) and the database values.

C#

if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}

var dbValues = (Department)databaseEntry.ToObject();


await SetDbErrorMessage(dbValues, clientValues, _context);

// Save the current ConcurrencyToken so next postback


// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.
{nameof(Department.ConcurrencyToken)}");
}
The following code adds a custom error message for each column that has database
values different from what was posted to OnPostAsync :

C#

private async Task SetDbErrorMessage(Department dbValues,


Department clientValues, SchoolContext context)
{

if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}

ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database
"
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}

The following highlighted code sets the ConcurrencyToken value to the new value
retrieved from the database. The next time the user clicks Save, only concurrency errors
that happen since the last display of the Edit page will be caught.

C#

if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}

var dbValues = (Department)databaseEntry.ToObject();


await SetDbErrorMessage(dbValues, clientValues, _context);

// Save the current ConcurrencyToken so next postback


// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.
{nameof(Department.ConcurrencyToken)}");
}

The ModelState.Remove statement is required because ModelState has the previous


ConcurrencyToken value. In the Razor Page, the ModelState value for a field takes

precedence over the model property values when both are present.

SQL Server vs SQLite code differences


The following shows the differences between the SQL Server and SQLite versions:

diff

+ using System; // For GUID on SQLite

+ departmentToUpdate.ConcurrencyToken = Guid.NewGuid();

_context.Entry(departmentToUpdate)
.Property(d => d.ConcurrencyToken).OriginalValue =
Department.ConcurrencyToken;

- Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
+ Department.ConcurrencyToken = dbValues.ConcurrencyToken;
Update the Edit Razor page
Update Pages/Departments/Edit.cshtml with the following code:

CSHTML

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.ConcurrencyToken" />
<div class="form-group">
<label>Version</label>
@Utility.GetLastChars(Model.Department.ConcurrencyToken)
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label">
</label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label">
</label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-
danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label">
</label>
<input asp-for="Department.StartDate" class="form-control"
/>
<span asp-validation-for="Department.StartDate" class="text-
danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-
control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID"
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-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

The preceding code:

Updates the page directive from @page to @page "{id:int}" .


Adds a hidden row version. ConcurrencyToken must be added so postback binds
the value.
Displays the last byte of ConcurrencyToken for debugging purposes.
Replaces ViewData with the strongly-typed InstructorNameSL .

Test concurrency conflicts with the Edit page


Open two browsers instances of Edit on the English department:

Run the app and select Departments.


Right-click the Edit hyperlink for the English department and select Open in new
tab.
In the first tab, click the Edit hyperlink for the English department.

The two browser tabs display the same information.

Change the name in the first browser tab and click Save.
The browser shows the Index page with the changed value and updated
ConcurrencyToken indicator. Note the updated ConcurrencyToken indicator, it's displayed

on the second postback in the other tab.

Change a different field in the second browser tab.


Click Save. You see error messages for all fields that don't match the database values:
This browser window didn't intend to change the Name field. Copy and paste the
current value (Languages) into the Name field. Tab out. Client-side validation removes
the error message.

Click Save again. The value you entered in the second browser tab is saved. You see the
saved values in the Index page.

Update the Delete page model


Update Pages/Departments/Delete.cshtml.cs with the following code:

C#

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;

public DeleteModel(ContosoUniversity.Data.SchoolContext context)


{
_context = context;
}

[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }

public async Task<IActionResult> OnGetAsync(int id, bool?


concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);

if (Department == null)
{
return NotFound();
}

if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to
delete "
+ "was modified by another user after you selected delete.
"
+ "The delete operation was canceled and the current
values in the "
+ "database have been displayed. If you still want to
delete this "
+ "record, click the Delete button again.";
}
return Page();
}

public async Task<IActionResult> OnPostAsync(int id)


{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.ConcurrencyToken value is from when the
entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}

The Delete page detects concurrency conflicts when the entity has changed after it was
fetched. Department.ConcurrencyToken is the row version when the entity was fetched.
When EF Core creates the SQL DELETE command, it includes a WHERE clause with
ConcurrencyToken . If the SQL DELETE command results in zero rows affected:

The ConcurrencyToken in the SQL DELETE command doesn't match


ConcurrencyToken in the database.
A DbUpdateConcurrencyException exception is thrown.
OnGetAsync is called with the concurrencyError .

Update the Delete Razor page


Update Pages/Departments/Delete.cshtml with the following code:

CSHTML

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

@{
ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Department</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.ConcurrencyToken)
</dt>
<dd class="col-sm-10">
@Utility.GetLastChars(Model.Department.ConcurrencyToken)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model =>
model.Department.Administrator.FullName)
</dd>
</dl>

<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.ConcurrencyToken" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>

The preceding code makes the following changes:

Updates the page directive from @page to @page "{id:int}" .


Adds an error message.
Replaces FirstMidName with FullName in the Administrator field.
Changes ConcurrencyToken to display the last byte.
Adds a hidden row version. ConcurrencyToken must be added so postback binds
the value.

Test concurrency conflicts


Create a test department.

Open two browsers instances of Delete on the test department:

Run the app and select Departments.


Right-click the Delete hyperlink for the test department and select Open in new
tab.
Click the Edit hyperlink for the test department.

The two browser tabs display the same information.

Change the budget in the first browser tab and click Save.

The browser shows the Index page with the changed value and updated
ConcurrencyToken indicator. Note the updated ConcurrencyToken indicator, it's displayed

on the second postback in the other tab.

Delete the test department from the second tab. A concurrency error is display with the
current values from the database. Clicking Delete deletes the entity, unless
ConcurrencyToken has been updated.

Additional resources
Concurrency Tokens in EF Core
Handle concurrency in EF Core
Debugging ASP.NET Core 2.x source

Next steps
This is the last tutorial in the series. Additional topics are covered in the MVC version of
this tutorial series.

Previous tutorial
ASP.NET Core MVC with EF Core -
tutorial series
Article • 04/11/2023

This tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and
views. Razor Pages is an alternative programming model. For new development, we
recommend Razor Pages over MVC with controllers and views. See the Razor Pages
version of this tutorial. Each tutorial covers some material the other doesn't:

Some things this MVC tutorial has that the Razor Pages tutorial doesn't:

Implement inheritance in the data model


Perform raw SQL queries
Use dynamic LINQ to simplify code

Some things the Razor Pages tutorial has that this one doesn't:

Use Select method to load related data


Best practices for EF.

1. Get started
2. Create, Read, Update, and Delete operations
3. Sorting, filtering, paging, and grouping
4. Migrations
5. Create a complex data model
6. Reading related data
7. Updating related data
8. Handle concurrency conflicts
9. Inheritance
10. Advanced topics
Tutorial: Get started with EF Core in an
ASP.NET MVC web app
Article • 04/11/2023

By Tom Dykstra and Rick Anderson

This tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and
views. Razor Pages is an alternative programming model. For new development, we
recommend Razor Pages over MVC with controllers and views. See the Razor Pages
version of this tutorial. Each tutorial covers some material the other doesn't:

Some things this MVC tutorial has that the Razor Pages tutorial doesn't:

Implement inheritance in the data model


Perform raw SQL queries
Use dynamic LINQ to simplify code

Some things the Razor Pages tutorial has that this one doesn't:

Use Select method to load related data


Best practices for EF.

The Contoso University sample web app demonstrates how to create an ASP.NET Core
MVC web app using Entity Framework (EF) Core and Visual Studio.

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 is the first
in a series of tutorials that explain how to build the Contoso University sample app.

Prerequisites
If you're new to ASP.NET Core MVC, go through the Get started with ASP.NET Core
MVC tutorial series before starting this one.

Visual Studio 2022 with the ASP.NET and web development workload.

This tutorial has not been updated for ASP.NET Core 6 or later. The tutorial's instructions
will not work correctly if you create a project that targets ASP.NET Core 6 or 7. For
example, the ASP.NET Core 6 and 7 web templates use the minimal hosting model,
which unifies Startup.cs and Program.cs into a single Program.cs file.
Another difference introduced in .NET 6 is the NRT (nullable reference types) feature.
The project templates enable this feature by default. Problems can happen where EF
considers a property to be required in .NET 6 which is nullable in .NET 5. For example,
the Create Student page will fail silently unless the Enrollments property is made
nullable or the asp-validation-summary helper tag is changed from ModelOnly to All .

We recommend that you install and use the .NET 5 SDK for this tutorial. Until this
tutorial is updated, see Razor Pages with Entity Framework Core in ASP.NET Core -
Tutorial 1 of 8 on how to use Entity Framework with ASP.NET Core 6 or later.

Database engines
The Visual Studio instructions use SQL Server LocalDB, a version of SQL Server Express
that runs only on Windows.

Solve problems and troubleshoot


If you run into a problem you can't resolve, you can generally find the solution by
comparing your code to the completed project . For a list of common errors and how
to solve them, see the Troubleshooting section of the last tutorial in the series. If you
don't find what you need there, you can post a question to StackOverflow.com for
ASP.NET Core or EF Core .

 Tip

This is a series of 10 tutorials, each of which builds on what is done in earlier


tutorials. Consider saving a copy of the project after each successful tutorial
completion. Then if you run into problems, you can start over from the previous
tutorial instead of going back to the beginning of the whole series.

Contoso University web app


The app built in these tutorials is a basic university web site.

Users can view and update student, course, and instructor information. Here are a few of
the screens in the app:
Create web app
1. Start Visual Studio and select Create a new project.
2. In the Create a new project dialog, select ASP.NET Core Web Application > Next.
3. In the Configure your new project dialog, enter ContosoUniversity for Project
name. It's important to use this exact name including capitalization, so each
namespace matches when code is copied.
4. Select Create.
5. In the Create a new ASP.NET Core web application dialog, select:
a. .NET Core and ASP.NET Core 5.0 in the dropdowns.
b. ASP.NET Core Web App (Model-View-Controller).
c. Create

Set up the site style


A few basic changes set up the site menu, layout, and home page.

Open Views/Shared/_Layout.cshtml and make the following changes:

Change each occurrence of ContosoUniversity to Contoso University . There are


three occurrences.
Add menu entries for About, Students, Courses, Instructors, and Departments,
and delete the Privacy menu entry.

The preceding changes are highlighted in the following code:


CSHTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</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-area="" asp-controller="Home"
asp-action="Index">Contoso University</a>
<button class="navbar-toggler" type="button" data-
toggle="collapse" data-target=".navbar-collapse" aria-
controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle
navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="About">About</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Students" asp-action="Index">Students</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Courses" asp-action="Index">Courses</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Instructors" asp-action="Index">Instructors</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Departments" asp-action="Index">Departments</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">


<div class="container">
&copy; 2020 - Contoso University - <a asp-area="" asp-
controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<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>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

In Views/Home/Index.cshtml , replace the contents of the file with the following markup:

CSHTML

@{
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 MVC web application.
</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.asp.net/en/latest/data/ef-mvc/intro.html">See the
tutorial &raquo;</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/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef
-mvc/intro/samples/5cu-final">See project source code &raquo;</a></p>
</div>
</div>

Press CTRL+F5 to run the project or choose Debug > Start Without Debugging from
the menu. The home page is displayed with tabs for the pages created in this tutorial.

EF Core NuGet packages


This tutorial uses SQL Server, and the provider package is
Microsoft.EntityFrameworkCore.SqlServer .

The EF SQL Server package and its dependencies, Microsoft.EntityFrameworkCore and


Microsoft.EntityFrameworkCore.Relational , provide runtime support for EF.
Add the Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet package. In
the Package Manager Console (PMC), enter the following commands to add the NuGet
packages:

PowerShell

Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer

The Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet package provides


ASP.NET Core middleware for EF Core error pages. This middleware helps to detect and
diagnose errors with EF Core migrations.

For information about other database providers that are available for EF Core, see
Database providers.

Create the data model


The following entity classes are created for this app:

The preceding entities have the following relationships:

A one-to-many relationship between Student and Enrollment entities. A student


can be enrolled in any number of courses.
A one-to-many relationship between Course and Enrollment entities. A course can
have any number of students enrolled in it.

In the following sections, a class is created for each of these entities.

The Student entity


In the Models folder, create the Student class with the following code:

C#

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

public ICollection<Enrollment> Enrollments { get; set; }


}
}

The ID property is the primary key (PK) column of the database table that corresponds
to this class. By default, EF interprets a property that's named ID or classnameID as the
primary key. For example, the PK could be named StudentID rather than ID .

The Enrollments property is a navigation property. Navigation properties hold other


entities that are related to this entity. The Enrollments property of a Student entity:

Contains all of the Enrollment entities that are related to that Student entity.
If a specific Student row in the database has two related Enrollment rows:
That Student entity's Enrollments navigation property contains those two
Enrollment entities.

Enrollment rows contain a student's PK value in the StudentID foreign key (FK) column.

If a navigation property can hold multiple entities:

The type must be a list, such as ICollection<T> , List<T> , or HashSet<T> .


Entities can be added, deleted, and updated.

Many-to-many and one-to-many navigation relationships can contain multiple entities.


When ICollection<T> is used, EF creates a HashSet<T> collection by default.

The Enrollment entity

In the Models folder, create the Enrollment class with the following code:

C#

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

The EnrollmentID property is the PK. This entity uses the classnameID pattern instead of
ID by itself. The Student entity used the ID pattern. Some developers prefer to use one

pattern throughout the data model. In this tutorial, the variation illustrates that either
pattern can be used. A later tutorial shows how using ID without classname makes it
easier to implement inheritance in the data model.
The Grade property is an enum . The ? 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 (FK), and the corresponding navigation property
is Student . An Enrollment entity is associated with one Student entity, so the property
can only hold a single Student entity. This differs from the Student.Enrollments
navigation property, which can hold multiple Enrollment entities.

The CourseID property is a FK, and the corresponding navigation property is Course . An
Enrollment entity is associated with one Course entity.

Entity Framework interprets a property as a FK property if it's named < navigation


property name >< primary key property name > . For example, StudentID for the Student
navigation property since the Student entity's PK is ID . FK properties can also be named
< primary key property name > . For example, CourseID because the Course entity's PK is

CourseID .

The Course entity

In the Models folder, create the Course class with the following code:

C#

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

public ICollection<Enrollment> Enrollments { get; set; }


}
}

The Enrollments property is a navigation property. A Course entity can be related to


any number of Enrollment entities.

The DatabaseGenerated attribute is explained in a later tutorial. This attribute allows


entering the PK for the course rather than having the database generate it.

Create the database context


The main class that coordinates EF functionality for a given data model is the DbContext
database context class. This class is created by deriving from the
Microsoft.EntityFrameworkCore.DbContext class. The DbContext derived class specifies

which entities are included in the data model. Some EF behaviors can be customized. In
this project, the class is named SchoolContext .

In the project folder, create a folder named Data .

In the Data folder create a SchoolContext class with the following code:

C#

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}

The preceding code creates a DbSet property for each entity set. In EF terminology:

An entity set typically corresponds to a database table.


An entity corresponds to a row in the table.
The DbSet<Enrollment> and DbSet<Course> statements could be omitted and it would
work the same. EF would include them implicitly because:

The Student entity references the Enrollment entity.


The Enrollment entity references the Course entity.

When the database is created, EF creates tables that have names the same as the DbSet
property names. Property names for collections are typically plural. For example,
Students rather than Student . Developers disagree about whether table names should
be pluralized or not. For these tutorials, the default behavior is overridden by specifying
singular table names in the DbContext . To do that, add the following highlighted code
after the last DbSet property.

C#

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}

Register the SchoolContext


ASP.NET Core includes dependency injection. Services, such as the EF database context,
are registered with dependency injection during app startup. Components that require
these services, such as MVC controllers, are provided these services via constructor
parameters. The controller constructor code that gets a context instance is shown later
in this tutorial.

To register SchoolContext as a service, open Startup.cs , and add the highlighted lines
to the ConfigureServices method.

C#

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ContosoUniversity
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<SchoolContext>(options =>

options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
);

services.AddControllersWithViews();
}

The name of the connection string is passed in to the context by calling a method on a
DbContextOptionsBuilder object. For local development, the ASP.NET Core configuration
system reads the connection string from the appsettings.json file.

Open the appsettings.json file and add a connection string as shown in the following
markup:

JSON
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;
MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

Add the database exception filter


Add AddDatabaseDeveloperPageExceptionFilter to ConfigureServices as shown in the
following code:

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<SchoolContext>(options =>

options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
);

services.AddDatabaseDeveloperPageExceptionFilter();

services.AddControllersWithViews();
}

The AddDatabaseDeveloperPageExceptionFilter provides helpful error information in the


development environment.

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.
Initialize DB with test data
EF creates an empty database. In this section, a method is added that's called after the
database is created in order to populate it with test data.

The EnsureCreated method is used to automatically create the database. In a later


tutorial, you see how to handle model changes by using Code First Migrations to
change the database schema instead of dropping and re-creating the database.

In the Data folder, create a new class named DbInitializer with the following code:

C#

using ContosoUniversity.Models;
using System;
using System.Linq;

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
}

var students = new Student[]


{
new
Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.P
arse("2005-09-01")},
new
Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Pa
rse("2002-09-01")},
new
Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse
("2003-09-01")},
new
Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Pa
rse("2002-09-01")},
new
Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002
-09-01")},
new
Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Pars
e("2001-09-01")},
new
Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse
("2003-09-01")},
new
Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Pars
e("2005-09-01")}
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();

var courses = new Course[]


{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollments.Add(e);
}
context.SaveChanges();
}
}
}

The preceding code checks if the database exists:


If the database is not found;
It is created and loaded with test data. It loads test data into arrays rather than
List<T> collections to optimize performance.

If the database is found, it takes no action.

Update Program.cs with the following code:

C#

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

private static void CreateDbIfNotExists(IHost host)


{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>
();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger =
services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the
DB.");
}
}
}

public static IHostBuilder CreateHostBuilder(string[] args) =>


Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

Program.cs does the following on app startup:

Get a database context instance from the dependency injection container.


Call the DbInitializer.Initialize method.
Dispose the context when the Initialize method completes as shown in the
following code:

C#

public static void Main(string[] args)


{
var host = CreateWebHostBuilder(args).Build();

using (var scope = host.Services.CreateScope())


{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the
database.");
}
}

host.Run();
}

The first time the app is run, the database is created and loaded with test data.
Whenever the data model changes:

Delete the database.


Update the seed method, and start afresh with a new database.

In later tutorials, the database is modified when the data model changes, without
deleting and re-creating it. No data is lost when the data model changes.
Create controller and views
Use the scaffolding engine in Visual Studio to add an MVC controller and views that will
use EF to query and save data.

The automatic creation of CRUD action methods and views is known as scaffolding.

In Solution Explorer, right-click the Controllers folder and select Add > New
Scaffolded Item.
In the Add Scaffold dialog box:
Select MVC controller with views, using Entity Framework.
Click Add. The Add MVC Controller with views, using Entity Framework dialog
box appears:

In Model class, select Student.


In Data context class, select SchoolContext.
Accept the default StudentsController as the name.
Click Add.

The Visual Studio scaffolding engine creates a StudentsController.cs file and a set of
views ( *.cshtml files) that work with the controller.

Notice the controller takes a SchoolContext as a constructor parameter.

C#

namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;

public StudentsController(SchoolContext context)


{
_context = context;
}

ASP.NET Core dependency injection takes care of passing an instance of SchoolContext


into the controller. You configured that in the Startup class.

The controller contains an Index action method, which displays all students in the
database. The method gets a list of students from the Students entity set by reading the
Students property of the database context instance:

C#

public async Task<IActionResult> Index()


{
return View(await _context.Students.ToListAsync());
}

The asynchronous programming elements in this code are explained later in the tutorial.

The Views/Students/Index.cshtml view displays this list in a table:

CSHTML

@model IEnumerable<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.LastName)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
@Html.DisplayNameFor(model => model.EnrollmentDate)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<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-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>

Press CTRL+F5 to run the project or choose Debug > Start Without Debugging from
the menu.

Click the Students tab to see the test data that the DbInitializer.Initialize method
inserted. Depending on how narrow your browser window is, you'll see the Students tab
link at the top of the page or you'll have to click the navigation icon in the upper right
corner to see the link.
View the database
When the app is started, the DbInitializer.Initialize method calls EnsureCreated . EF
saw that there was no database:

So it created a database.
The Initialize method code populated the database with data.

Use SQL Server Object Explorer (SSOX) to view the database in Visual Studio:

Select SQL Server Object Explorer from the View menu in Visual Studio.
In SSOX, select (localdb)\MSSQLLocalDB > Databases.
Select ContosoUniversity1 , the entry for the database name that's in the
connection string in the appsettings.json file.
Expand the Tables node to see the tables in the database.

Right-click the Student table and click View Data to see the data in the table.

The *.mdf and *.ldf database files are in the C:\Users\<username> folder.

Because EnsureCreated is called in the initializer method that runs on app start, you
could:

Make a change to the Student class.


Delete the database.
Stop, then start the app. The database is automatically re-created to match the
change.

For example, if an EmailAddress property is added to the Student class, a new


EmailAddress column in the re-created table. The view won't display the new
EmailAddress property.

Conventions
The amount of code written in order for the EF to create a complete database is minimal
because of the use of the conventions EF uses:

The names of DbSet properties are used as table names. For entities not
referenced by a DbSet property, entity class names are used as table names.
Entity property names are used for column names.
Entity properties that are named ID or classnameID are recognized as PK
properties.
A property is interpreted as a FK property if it's named < navigation property
name >< PK property name > . For example, StudentID for the Student navigation
property since the Student entity's PK is ID . FK properties can also be named
< primary key property name > . For example, EnrollmentID since the Enrollment

entity's PK is EnrollmentID .

Conventional behavior can be overridden. For example, table names can be explicitly
specified, as shown earlier in this tutorial. Column names and any property can be set as
a PK or FK.

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, but for low
traffic situations the performance hit is negligible, while for high traffic situations, the
potential performance improvement is substantial.

In the following code, async , Task<T> , await , and ToListAsync make the code execute
asynchronously.

C#

public async Task<IActionResult> Index()


{
return View(await _context.Students.ToListAsync());
}

The async keyword tells the compiler to generate callbacks for parts of the
method body and to automatically create the Task<IActionResult> object that's
returned.
The return type Task<IActionResult> represents ongoing work with a result of type
IActionResult .

The await keyword causes the compiler to split the method into two parts. The
first part ends with the operation that's started asynchronously. The second part is
put into a callback method that's called when the operation completes.
ToListAsync is the asynchronous version of the ToList extension method.

Some things to be aware of when writing asynchronous code that uses EF:

Only statements that cause queries or commands to be sent to the database are
executed asynchronously. That includes, for example, ToListAsync ,
SingleOrDefaultAsync , and SaveChangesAsync . It doesn't include, for example,

statements that just change an IQueryable , such as var students =


context.Students.Where(s => s.LastName == "Davolio") .

An EF context isn't thread safe: don't try to do multiple operations in parallel.


When you call any async EF method, always use the await keyword.
To take advantage of the performance benefits of async code, make sure that any
library packages used also use async if they call any EF methods that cause queries
to be sent to the database.

For more information about asynchronous programming in .NET, see Async Overview.

Limit entities fetched


See Performance considerations for information on limiting the number of entities
returned from a query.

SQL Logging of Entity Framework Core


Logging configuration is commonly provided by the Logging section of appsettings.
{Environment}.json files. To log SQL statements, add

"Microsoft.EntityFrameworkCore.Database.Command": "Information" to the


appsettings.Development.json file:

JSON

{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB-
2;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
"AllowedHosts": "*"
}

With the preceding JSON, SQL statements are displayed on the command line and in
the Visual Studio output window.

For more information, see Logging in .NET Core and ASP.NET Core and this GitHub
issue .

Advance to the next tutorial to learn how to perform basic CRUD (create, read, update,
delete) operations.

Implement basic CRUD functionality


Tutorial: Implement CRUD Functionality
- ASP.NET MVC with EF Core
Article • 04/11/2023

In the previous tutorial, you created an MVC application that stores and displays data
using the Entity Framework and SQL Server LocalDB. In this tutorial, you'll review and
customize the CRUD (create, read, update, delete) code that the MVC scaffolding
automatically creates for you in controllers and views.

7 Note

It's a common practice to implement the repository pattern in order to create an


abstraction layer between your controller and the data access layer. To keep these
tutorials simple and focused on teaching how to use the Entity Framework itself,
they don't use repositories. For information about repositories with EF, see the last
tutorial in this series.

In this tutorial, you:

" Customize the Details page


" Update the Create page
" Update the Edit page
" Update the Delete page
" Close database connections

Prerequisites
Get started with EF Core and ASP.NET Core MVC

Customize the Details page


The scaffolded code for the Students Index page left out the Enrollments property,
because that property holds a collection. In the Details page, you'll display the contents
of the collection in an HTML table.

In Controllers/StudentsController.cs , the action method for the Details view uses the
FirstOrDefaultAsync method to retrieve a single Student entity. Add code that calls
Include . ThenInclude , and AsNoTracking methods, as shown in the following

highlighted code.

C#

public async Task<IActionResult> Details(int? id)


{
if (id == null)
{
return NotFound();
}

var student = await _context.Students


.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);

if (student == null)
{
return NotFound();
}

return View(student);
}

The Include and ThenInclude methods cause the context to load the
Student.Enrollments navigation property, and within each enrollment the

Enrollment.Course navigation property. You'll learn more about these methods in the

read related data tutorial.

The AsNoTracking method improves performance in scenarios where the entities


returned won't be updated in the current context's lifetime. You'll learn more about
AsNoTracking at the end of this tutorial.

Route data
The key value that's passed to the Details method comes from route data. Route data
is data that the model binder found in a segment of the URL. For example, the default
route specifies controller, action, and id segments:

C#

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

In the following URL, the default route maps Instructor as the controller, Index as the
action, and 1 as the id; these are route data values.

http://localhost:1230/Instructor/Index/1?courseID=2021

The last part of the URL ("?courseID=2021") is a query string value. The model binder
will also pass the ID value to the Index method id parameter if you pass it as a query
string value:

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

In the Index page, hyperlink URLs are created by tag helper statements in the Razor
view. In the following Razor code, the id parameter matches the default route, so id is
added to the route data.

HTML

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>

This generates the following HTML when item.ID is 6:

HTML

<a href="/Students/Edit/6">Edit</a>

In the following Razor code, studentID doesn't match a parameter in the default route,
so it's added as a query string.

HTML

<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>

This generates the following HTML when item.ID is 6:

HTML
<a href="/Students/Edit?studentID=6">Edit</a>

For more information about tag helpers, see Tag Helpers in ASP.NET Core.

Add enrollments to the Details view


Open Views/Students/Details.cshtml . Each field is displayed using DisplayNameFor and
DisplayFor helpers, as shown in the following example:

CSHTML

<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.LastName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.LastName)
</dd>

After the last field and immediately before the closing </dl> tag, add the following
code to display a list of enrollments:

CSHTML

<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd class="col-sm-10">
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>

If code indentation is wrong after you paste the code, press CTRL-K-D to correct it.
This 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. You see the
list of courses and grades for the selected student:

Update the Create page


In StudentsController.cs , modify the HttpPost Create method by adding a try-catch
block and removing ID from the Bind attribute.

C#

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
try
{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");
}
return View(student);
}

This code adds the Student entity created by the ASP.NET Core MVC model binder to
the Students entity set and then saves the changes to the database. (Model binder refers
to the ASP.NET Core MVC functionality that makes it easier for you to work with data
submitted by a form; a model binder converts posted form values to CLR types and
passes them to the action method in parameters. In this case, the model binder
instantiates a Student entity for you using property values from the Form collection.)

You removed ID from the Bind attribute because ID is the primary key value which SQL
Server will set automatically when the row is inserted. Input from the user doesn't set
the ID value.

Other than the Bind attribute, the try-catch block is the only change you've made to the
scaffolded code. If an exception that derives from DbUpdateException is caught while the
changes are being saved, a generic error message is displayed. DbUpdateException
exceptions are sometimes caused by something external to the application rather than a
programming error, so the user is advised to try again. Although not implemented in
this sample, a production quality application would log the exception. For more
information, see the Log for insight section in Monitoring and Telemetry (Building Real-
World Cloud Apps with Azure).

The ValidateAntiForgeryToken attribute helps prevent cross-site request forgery (CSRF)


attacks. The token is automatically injected into the view by the FormTagHelper and is
included when the form is submitted by the user. The token is validated by the
ValidateAntiForgeryToken attribute. For more information, see Prevent Cross-Site
Request Forgery (XSRF/CSRF) attacks in ASP.NET Core.

Security note about overposting


The Bind attribute that the scaffolded code includes on the Create method is one way
to protect against overposting in create scenarios. For example, suppose the Student
entity includes a Secret property that you don't want this web page to set.
C#

public class Student


{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}

Even if you don't have a Secret field on the web page, a hacker could use a tool such as
Fiddler, or write some JavaScript, to post a Secret form value. Without the Bind
attribute limiting the fields that the model binder uses when it creates a Student
instance, the model binder would pick up that Secret form value and use it to create
the Student entity instance. Then whatever value the hacker specified for the Secret
form field would be updated in your database. The following image shows the Fiddler
tool adding the Secret field (with the value "OverPost") to the posted form values.

The value "OverPost" would then be successfully added to the Secret property of the
inserted row, although you never intended that the web page be able to set that
property.

You can prevent overposting in edit scenarios by reading the entity from the database
first and then calling TryUpdateModel , passing in an explicit allowed properties list. That's
the method used in these tutorials.
An alternative way to prevent overposting that's preferred by many developers is to use
view models rather than entity classes with model binding. Include only the properties
you want to update in the view model. Once the MVC model binder has finished, copy
the view model properties to the entity instance, optionally using a tool such as
AutoMapper. Use _context.Entry on the entity instance to set its state to Unchanged ,
and then set Property("PropertyName").IsModified to true on each entity property that's
included in the view model. This method works in both edit and create scenarios.

Test the Create page


The code in Views/Students/Create.cshtml uses label , input , and span (for validation
messages) tag helpers for each field.

Run the app, select the Students tab, and click Create New.

Enter names and a date. Try entering an invalid date if your browser lets you do that.
(Some browsers force you to use a date picker.) Then click Create to see the error
message.
This is server-side validation that you get by default; in a later tutorial you'll see how to
add attributes that will generate code for client-side validation also. The following
highlighted code shows the model validation check in the Create method.

C#

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
try
{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");
}
return View(student);
}

Change the date to a valid value and click Create to see the new student appear in the
Index page.

Update the Edit page


In StudentController.cs , the HttpGet Edit method (the one without the HttpPost
attribute) uses the FirstOrDefaultAsync method to retrieve the selected Student entity,
as you saw in the Details method. You don't need to change this method.

Recommended HttpPost Edit code: Read and update


Replace the HttpPost Edit action method with the following code.

C#

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
var studentToUpdate = await _context.Students.FirstOrDefaultAsync(s =>
s.ID == id);
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
}
return View(studentToUpdate);
}

These changes implement a security best practice to prevent overposting. The scaffolder
generated a Bind attribute and added the entity created by the model binder to the
entity set with a Modified flag. That code isn't recommended for many scenarios
because the Bind attribute clears out any pre-existing data in fields not listed in the
Include parameter.

The new code reads the existing entity and calls TryUpdateModel to update fields in the
retrieved entity based on user input in the posted form data. The Entity Framework's
automatic change tracking sets the Modified flag on the fields that are changed by form
input. When the SaveChanges method is called, the Entity Framework creates SQL
statements to update the database row. Concurrency conflicts are ignored, and only the
table columns that were updated by the user are updated in the database. (A later
tutorial shows how to handle concurrency conflicts.)

As a best practice to prevent overposting, the fields that you want to be updateable by
the Edit page are declared in the TryUpdateModel parameters. (The empty string
preceding the list of fields in the parameter list is for a prefix to use with the form fields
names.) Currently there are no extra fields that you're protecting, but listing the fields
that you want the model binder to bind ensures that if you add fields to the data model
in the future, they're automatically protected until you explicitly add them here.

As a result of these changes, the method signature of the HttpPost Edit method is the
same as the HttpGet Edit method; therefore you've renamed the method EditPost .

Alternative HttpPost Edit code: Create and attach


The recommended HttpPost edit code ensures that only changed columns get updated
and preserves data in properties that you don't want included for model binding.
However, the read-first approach requires an extra database read, and can result in
more complex code for handling concurrency conflicts. An alternative is to attach an
entity created by the model binder to the EF context and mark it as modified. (Don't
update your project with this code, it's only shown to illustrate an optional approach.)

C#
public async Task<IActionResult> Edit(int id,
[Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
{
if (id != student.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
}
return View(student);
}

You can use this approach when the web page UI includes all of the fields in the entity
and can update any of them.

The scaffolded code uses the create-and-attach approach but only catches
DbUpdateConcurrencyException exceptions and returns 404 error codes. The example

shown catches any database update exception and displays an error message.

Entity States
The database context keeps track of whether entities in memory are in sync with their
corresponding rows in the database, and this information determines what happens
when you call the SaveChanges method. For example, when you pass a new entity to the
Add method, that entity's state is set to Added . Then when you call the SaveChanges

method, 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 . Nothing needs to be done with this entity by the SaveChanges method.

When you read an entity from the database, the entity starts out with this status.

Modified . Some or all of the entity's property values have been modified. The

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 application, state changes are typically set automatically. You read an entity
and make changes to some of its property values. This causes its entity state to
automatically be changed to Modified . Then when you call SaveChanges , the Entity
Framework generates a SQL UPDATE statement that updates only the actual properties
that you changed.

In a web app, the DbContext that initially reads an entity and displays its data to be
edited is disposed after a page is rendered. When the HttpPost Edit action method is
called, a new web request is made and you have a new instance of the DbContext . If you
re-read the entity in that new context, you simulate desktop processing.

But if you don't want to do the extra read operation, you have to use the entity object
created by the model binder. The simplest way to do this is to set the entity state to
Modified as is done in the alternative HttpPost Edit code shown earlier. Then when you
call SaveChanges , the Entity Framework updates all columns of the database row,
because the context has no way to know which properties you changed.

If you want to avoid the read-first approach, but you also want the SQL UPDATE
statement to update only the fields that the user actually changed, the code is more
complex. You have to save the original values in some way (such as by using hidden
fields) so that they're available when the HttpPost Edit method is called. Then you can
create a Student entity using the original values, call the Attach method with that
original version of the entity, update the entity's values to the new values, and then call
SaveChanges .

Test the Edit page


Run the app, select the Students tab, then click an Edit hyperlink.
Change some of the data and click Save. The Index page opens and you see the
changed data.

Update the Delete page


In StudentController.cs , the template code for the HttpGet Delete method uses the
FirstOrDefaultAsync method to retrieve the selected Student entity, as you saw in the
Details and Edit methods. However, to implement a custom error message when the call
to SaveChanges fails, you'll add some functionality to this method and its corresponding
view.

As you saw for update and create operations, delete operations require two action
methods. The method that's called in response to a GET request displays a view that
gives the user a chance to approve or cancel the delete operation. If the user approves
it, a POST request is created. When that happens, the HttpPost Delete method is called
and then that method actually performs the delete operation.
You'll add a try-catch block to the HttpPost Delete method to handle any errors that
might occur when the database is updated. If an error occurs, the HttpPost Delete
method calls the HttpGet Delete method, passing it a parameter that indicates that an
error has occurred. The HttpGet Delete method then redisplays the confirmation page
along with the error message, giving the user an opportunity to cancel or try again.

Replace the HttpGet Delete action method with the following code, which manages
error reporting.

C#

public async Task<IActionResult> Delete(int? id, bool? saveChangesError =


false)
{
if (id == null)
{
return NotFound();
}

var student = await _context.Students


.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (student == null)
{
return NotFound();
}

if (saveChangesError.GetValueOrDefault())
{
ViewData["ErrorMessage"] =
"Delete failed. Try again, and if the problem persists " +
"see your system administrator.";
}

return View(student);
}

This code accepts an optional parameter that indicates whether the method was called
after a failure to save changes. This parameter is false when the HttpGet Delete method
is called without a previous failure. When it's called by the HttpPost Delete method in
response to a database update error, the parameter is true and an error message is
passed to the view.

The read-first approach to HttpPost Delete


Replace the HttpPost Delete action method (named DeleteConfirmed ) with the
following code, which performs the actual delete operation and catches any database
update errors.

C#

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var student = await _context.Students.FindAsync(id);
if (student == null)
{
return RedirectToAction(nameof(Index));
}

try
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id,
saveChangesError = true });
}
}

This 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.

The create-and-attach approach to HttpPost Delete


If improving performance in a high-volume application is a priority, you could avoid an
unnecessary SQL query by instantiating a Student entity using only the primary key
value and then setting the entity state to Deleted . That's all that the Entity Framework
needs in order to delete the entity. (Don't put this code in your project; it's here just to
illustrate an alternative.)

C#

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
try
{
Student studentToDelete = new Student() { ID = id };
_context.Entry(studentToDelete).State = EntityState.Deleted;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id,
saveChangesError = true });
}
}

If the entity has related data that should also be deleted, make sure that cascade delete
is configured in the database. With this approach to entity deletion, EF might not realize
there are related entities to be deleted.

Update the Delete view


In Views/Student/Delete.cshtml , add an error message between the h2 heading and the
h3 heading, as shown in the following example:

CSHTML

<h2>Delete</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>

Run the app, select the Students tab, and click a Delete hyperlink:
Click Delete. The Index page is displayed without the deleted student. (You'll see an
example of the error handling code in action in the concurrency tutorial.)

Close database connections


To free up the resources that a database connection holds, the context instance must be
disposed as soon as possible when you are done with it. The ASP.NET Core built-in
dependency injection takes care of that task for you.

In Startup.cs , you call the AddDbContext extension method to provision the


DbContext class in the ASP.NET Core DI container. That method sets the service lifetime
to Scoped by default. Scoped means the context object lifetime coincides with the web
request life time, and the Dispose method will be called automatically at the end of the
web request.

Handle transactions
By default the Entity Framework implicitly implements transactions. In scenarios where
you make changes to multiple rows or tables and then call SaveChanges , the Entity
Framework automatically makes sure that either all of your changes succeed or they all
fail. If some changes are done first and then an error happens, those changes are
automatically rolled back. For scenarios where you need more control -- for example, if
you want to include operations done outside of Entity Framework in a transaction -- see
Transactions.

No-tracking queries
When a database context retrieves table rows and creates entity objects that represent
them, by default it keeps track of whether the entities in memory are in sync with what's
in the database. The data in memory acts as a cache and is used when you update an
entity. This caching is often unnecessary in a web application because context instances
are typically short-lived (a new one is created and disposed for each request) and the
context that reads an entity is typically disposed before that entity is used again.

You can disable tracking of entity objects in memory by calling the AsNoTracking
method. Typical scenarios in which you might want to do that include the following:

During the context lifetime you don't need to update any entities, and you don't
need EF to automatically load navigation properties with entities retrieved by
separate queries. Frequently these conditions are met in a controller's HttpGet
action methods.

You are running a query that retrieves a large volume of data, and only a small
portion of the returned data will be updated. It may be more efficient to turn off
tracking for the large query, and run a query later for the few entities that need to
be updated.

You want to attach an entity in order to update it, but earlier you retrieved the
same entity for a different purpose. Because the entity is already being tracked by
the database context, you can't attach the entity that you want to change. One way
to handle this situation is to call AsNoTracking on the earlier query.

For more information, see Tracking vs. No-Tracking.

Get the code


Download or view the completed application.
Next steps
In this tutorial, you:

" Customized the Details page


" Updated the Create page
" Updated the Edit page
" Updated the Delete page
" Closed database connections

Advance to the next tutorial to learn how to expand the functionality of the Index page
by adding sorting, filtering, and paging.

Next: Sorting, filtering, and paging


Tutorial: Add sorting, filtering, and
paging - ASP.NET MVC with EF Core
Article • 04/11/2023

In the previous tutorial, you implemented a set of web pages for basic CRUD operations
for Student entities. In this tutorial you'll add sorting, filtering, and paging functionality
to the Students Index page. You'll also create a page that does simple grouping.

The following illustration shows what the page will look like when you're done. The
column headings are links that the user can click to sort by that column. Clicking a
column heading repeatedly toggles between ascending and descending sort order.

In this tutorial, you:

" Add column sort links


" Add a Search box
" Add paging to Students Index
" Add paging to Index method
" Add paging links
" Create an About page

Prerequisites
Implement CRUD Functionality

Add column sort links


To add sorting to the Student Index page, you'll change the Index method of the
Students controller and add code to the Student Index view.

Add sorting Functionality to the Index method


In StudentsController.cs , replace the Index method with the following code:

C#

public async Task<IActionResult> Index(string sortOrder)


{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc"
: "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}

This code receives a sortOrder parameter from the query string in the URL. The query
string value is provided by ASP.NET Core MVC as a parameter to the action method. The
parameter will be a string that's either "Name" or "Date", optionally followed by an
underscore and the string "desc" to specify descending order. The default sort order is
ascending.

The first time the Index page is requested, there's no query string. The students are
displayed in ascending order by last name, which is the default as established by the
fall-through case in the switch statement. When the user clicks a column heading
hyperlink, the appropriate sortOrder value is provided in the query string.

The two ViewData elements (NameSortParm and DateSortParm) are used by the view to
configure the column heading hyperlinks with the appropriate query string values.

C#

public async Task<IActionResult> Index(string sortOrder)


{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc"
: "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}

These are ternary statements. The first one specifies that if the sortOrder parameter is
null or empty, NameSortParm should be set to "name_desc"; otherwise, it should be set
to an empty string. These two statements enable the view to set the column heading
hyperlinks as follows:

Current sort order Last Name Hyperlink Date Hyperlink

Last Name ascending descending ascending

Last Name descending ascending ascending


Current sort order Last Name Hyperlink Date Hyperlink

Date ascending ascending descending

Date descending ascending ascending

The method uses LINQ to Entities to specify the column to sort by. The code creates an
IQueryable variable before the switch statement, modifies it in the switch statement,

and calls the ToListAsync method after the switch statement. When you create and
modify IQueryable variables, no query is sent to the database. The query isn't executed
until you convert the IQueryable object into a collection by calling a method such as
ToListAsync . Therefore, this code results in a single query that's not executed until the
return View statement.

This code could get verbose with a large number of columns. The last tutorial in this
series shows how to write code that lets you pass the name of the OrderBy column in a
string variable.

Add column heading hyperlinks to the Student Index


view
Replace the code in Views/Students/Index.cshtml , with the following code to add
column heading hyperlinks. The changed lines are highlighted.

CSHTML

@model IEnumerable<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-
sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model =>
model.LastName)</a>
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
<a asp-action="Index" asp-route-
sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model =>
model.EnrollmentDate)</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<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-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>

This code uses the information in ViewData properties to set up hyperlinks with the
appropriate query string values.

Run the app, select the Students tab, and click the Last Name and Enrollment Date
column headings to verify that sorting works.
Add a Search box
To add filtering to the Students Index page, you'll add a text box and a submit button to
the view and make corresponding changes in the Index method. The text box will let
you enter a string to search for in the first name and last name fields.

Add filtering functionality to the Index method


In StudentsController.cs , replace the Index method with the following code (the
changes are highlighted).

C#

public async Task<IActionResult> Index(string sortOrder, string


searchString)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc"
: "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
ViewData["CurrentFilter"] = searchString;

var students = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}

You've added a searchString parameter to the Index method. The search string value is
received from a text box that you'll add to the Index view. You've also added to the LINQ
statement a where clause that selects only students whose first name or last name
contains the search string. The statement that adds the where clause is executed only if
there's a value to search for.

7 Note

Here you are calling the Where method on an IQueryable object, and the filter will
be processed on the server. In some scenarios you might be calling the Where
method as an extension method on an in-memory collection. (For example,
suppose you change the reference to _context.Students so that instead of an EF
DbSet it references a repository method that returns an IEnumerable collection.)
The result would normally be the same but in some cases may be different.

For example, the .NET Framework implementation of the Contains method


performs a case-sensitive comparison by default, but in SQL Server this is
determined by the collation setting of the SQL Server instance. That setting defaults
to case-insensitive. You could call the ToUpper method to make the test explicitly
case-insensitive: Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper()).
That would ensure that results stay the same if you change the code later to use a
repository which returns an IEnumerable collection instead of an IQueryable object.
(When you call the Contains method on an IEnumerable collection, you get the
.NET Framework implementation; when you call it on an IQueryable object, you get
the database provider implementation.) However, there's a performance penalty for
this solution. The ToUpper code would put a function in the WHERE clause of the
TSQL SELECT statement. That would prevent the optimizer from using an index.
Given that SQL is mostly installed as case-insensitive, it's best to avoid the ToUpper
code until you migrate to a case-sensitive data store.

Add a Search Box to the Student Index View


In Views/Student/Index.cshtml , add the highlighted code immediately before the
opening table tag in order to create a caption, a text box, and a Search button.

CSHTML

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">


<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString"
value="@ViewData["CurrentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>

<table class="table">

This 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, which means that parameters are
passed in the HTTP message body and not in the URL as query strings. When you
specify HTTP GET, the form data is passed in the URL as query strings, which enables
users to bookmark the URL. The W3C guidelines recommend that you should use GET
when the action doesn't result in an update.

Run the app, select the Students tab, enter a search string, and click Search to verify that
filtering is working.
Notice that the URL contains the search string.

HTML

http://localhost:5813/Students?SearchString=an

If you bookmark this page, you'll get the filtered list when you use the bookmark.
Adding method="get" to the form tag is what caused the query string to be generated.

At this stage, if you click a column heading sort link you'll lose the filter value that you
entered in the Search box. You'll fix that in the next section.

Add paging to Students Index


To add paging to the Students Index page, you'll create a PaginatedList class that uses
Skip and Take statements to filter data on the server instead of always retrieving all
rows of the table. Then you'll make additional changes in the Index method and add
paging buttons to the Index view. The following illustration shows the paging buttons.
In the project folder, create PaginatedList.cs , and then replace the template code with
the following code.

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }

public PaginatedList(List<T> items, int count, int pageIndex, int


pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);

this.AddRange(items);
}
public bool HasPreviousPage => PageIndex > 1;

public bool HasNextPage => PageIndex < TotalPages;

public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T>


source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageIndex - 1) *
pageSize).Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}

The CreateAsync method in this 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 will return a List containing only the requested page. The
properties HasPreviousPage and HasNextPage can be used to enable or disable Previous
and Next paging buttons.

A CreateAsync method is used instead of a constructor to create the PaginatedList<T>


object because constructors can't run asynchronous code.

Add paging to Index method


In StudentsController.cs , replace the Index method with the following code.

C#

public async Task<IActionResult> Index(


string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc"
: "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}

ViewData["CurrentFilter"] = searchString;

var students = from s in _context.Students


select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}

int pageSize = 3;
return View(await
PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1,
pageSize));
}

This code adds a page number parameter, a current sort order parameter, and a current
filter parameter to the method signature.

C#

public async Task<IActionResult> Index(


string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
The first time the page is displayed, or if the user hasn't clicked a paging or sorting link,
all the parameters will be null. If a paging link is clicked, the page variable will contain
the page number to display.

The ViewData element named CurrentSort provides the view with the current sort order,
because this must be included in the paging links in order to keep the sort order the
same while paging.

The ViewData element named CurrentFilter provides the view with the current filter
string. This value must be included in the paging links in order to maintain the filter
settings during paging, and it must be restored to the text box when the page is
redisplayed.

If the search string is changed during paging, the page has to be reset to 1, because the
new filter can result in different data to display. The search string is changed when a
value is entered in the text box and the Submit button is pressed. In that case, the
searchString parameter isn't null.

C#

if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}

At the end of the Index method, 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 then passed to the view.

C#

return View(await
PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1,
pageSize));

The PaginatedList.CreateAsync method takes a page number. The two question marks
represent the null-coalescing operator. The null-coalescing operator defines a default
value for a nullable type; the expression (pageNumber ?? 1) means return the value of
pageNumber if it has a value, or return 1 if pageNumber is null.
Add paging links
In Views/Students/Index.cshtml , replace the existing code with the following code. The
changes are highlighted.

CSHTML

@model PaginatedList<ContosoUniversity.Models.Student>

@{
ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
<a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">


<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString"
value="@ViewData["CurrentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>

<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-
sortOrder="@ViewData["NameSortParm"]" asp-route-
currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
</th>
<th>
First Name
</th>
<th>
<a asp-action="Index" asp-route-
sortOrder="@ViewData["DateSortParm"]" asp-route-
currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<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-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>

@{
var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex + 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @nextDisabled">
Next
</a>

The @model statement at the top of the page specifies that the view now gets a
PaginatedList<T> object instead of a List<T> object.

The column header links use the query string to pass the current search string to the
controller so that the user can sort within filter results:

HTML

<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-


route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>
The paging buttons are displayed by tag helpers:

HTML

<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>

Run the app and go to the Students page.

Click the paging links in different sort orders to make sure paging works. Then enter a
search string and try paging again to verify that paging also works correctly with sorting
and filtering.

Create an About page


For the Contoso University website's About page, you'll display how many students
have enrolled for each enrollment date. This requires grouping and simple calculations
on the groups. To accomplish this, you'll do the following:

Create a view model class for the data that you need to pass to the view.
Create the About method in the Home controller.
Create the About view.

Create the view model


Create a SchoolViewModels folder in the Models folder.

In the new folder, add a class file EnrollmentDateGroup.cs and replace the template
code with the following code:

C#

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }

public int StudentCount { get; set; }


}
}

Modify the Home Controller


In HomeController.cs , add the following using statements at the top of the file:

C#

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.Extensions.Logging;

Add a class variable for the database context immediately after the opening curly brace
for the class, and get an instance of the context from ASP.NET Core DI:

C#
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly SchoolContext _context;

public HomeController(ILogger<HomeController> logger, SchoolContext


context)
{
_logger = logger;
_context = context;
}

Add an About method with the following code:

C#

public async Task<ActionResult> About()


{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(await data.AsNoTracking().ToListAsync());
}

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.

Create the About View


Add a Views/Home/About.cshtml file with the following code:

CSHTML

@model
IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>

@{
ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>


<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>

@foreach (var item in Model)


{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>

Run the app and go to the About page. The count of students for each enrollment date
is displayed in a table.

Get the code


Download or view the completed application.

Next steps
In this tutorial, you:

" Added column sort links


" Added a Search box
" Added paging to Students Index
" Added paging to Index method
" Added paging links
" Created an About page

Advance to the next tutorial to learn how to handle data model changes by using
migrations.

Next: Handle data model changes


Tutorial: Part 5, apply migrations to the
Contoso University sample
Article • 04/11/2023

In this tutorial, you start using the EF Core migrations feature for managing data model
changes. In later tutorials, you'll add more migrations as you change the data model.

In this tutorial, you:

" Learn about migrations


" Create an initial migration
" Examine Up and Down methods
" Learn about the data model snapshot
" Apply the migration

Prerequisites
Sorting, filtering, and paging

About migrations
When you develop a new application, your data model changes frequently, and each
time the model changes, it gets out of sync with the database. You started these
tutorials by configuring the Entity Framework to create the database if it doesn't exist.
Then each time you change the data model -- add, remove, or change entity classes or
change your DbContext class -- you can delete the database and EF creates a new one
that matches the model, and seeds it with test data.

This method of keeping the database in sync with the data model works well until you
deploy the application to production. When the application is running in production it's
usually storing data that you want to keep, and you don't want to lose everything each
time you make a change such as adding a new column. The EF Core Migrations feature
solves this problem by enabling EF to update the database schema instead of creating a
new database.

To work with migrations, you can use the Package Manager Console (PMC) or the CLI.
These tutorials show how to use CLI commands. Information about the PMC is at the
end of this tutorial.
Drop the database
Install EF Core tools as a global tool and delete the database:

.NET CLI

dotnet tool install --global dotnet-ef


dotnet ef database drop

The following section explains how to run CLI commands.

Create an initial migration


Save your changes and build the project. Then open a command window and navigate
to the project folder. Here's a quick way to do that:

In Solution Explorer, right-click the project and choose Open Folder in File
Explorer from the context menu.

Enter "cmd" in the address bar and press Enter.


Enter the following command in the command window:

.NET CLI

dotnet ef migrations add InitialCreate

In the preceding commands, output similar to the following is displayed:

Console

info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'

If you see an error message "cannot access the file ... ContosoUniversity.dll because it is
being used by another process.", find the IIS Express icon in the Windows System Tray,
and right-click it, then click ContosoUniversity > Stop Site.

Examine Up and Down methods


When you executed the migrations add command, EF generated the code that will
create the database from scratch. This code is in the Migrations folder, in the file named
<timestamp>_InitialCreate.cs . The Up method of the InitialCreate class creates the
database tables that correspond to the data model entity sets, and the Down method
deletes them, as shown in the following example.

C#
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Course",
columns: table => new
{
CourseID = table.Column<int>(nullable: false),
Credits = table.Column<int>(nullable: false),
Title = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Course", x => x.CourseID);
});

// Additional code not shown


}

protected override void Down(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropTable(
name: "Enrollment");
// Additional code not shown
}
}

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.

This code is for the initial migration that was created when you entered the migrations
add InitialCreate command. The migration name parameter ("InitialCreate" in the

example) is used for the file name and can be whatever you want. It's best to choose a
word or phrase that summarizes what is being done in the migration. For example, you
might name a later migration "AddDepartmentTable".

If you created the initial migration when the database already exists, the database
creation code is generated but it doesn't have to run because the database already
matches the data model. When you deploy the app to another environment where the
database doesn't exist yet, this code will run to create your database, so it's a good idea
to test it first. That's why you dropped the database earlier -- so that migrations can
create a new one from scratch.

The data model snapshot


Migrations creates 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.

Use the dotnet ef migrations remove command to remove a migration. dotnet ef


migrations remove deletes the migration and ensures the snapshot is correctly reset. If

dotnet ef migrations remove fails, use dotnet ef migrations remove -v to get more

information on the failure.

See EF Core Migrations in Team Environments for more information about how the
snapshot file is used.

Apply the migration


In the command window, enter the following command to create the database and
tables in it.

.NET CLI

dotnet ef database update

The output from the command is similar to the migrations add command, except that
you see logs for the SQL commands that set up the database. Most of the logs are
omitted in the following sample output. If you prefer not to see this level of detail in log
messages, you can change the log level in the appsettings.Development.json file. For
more information, see Logging in .NET Core and ASP.NET Core.

text

info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (274ms) [Parameters=[], CommandType='Text',
CommandTimeout='60']
CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (60ms) [Parameters=[], CommandType='Text',
CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [ContosoUniversity2] SET READ_COMMITTED_SNAPSHOT
ON;
END;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (15ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);

<logs omitted for brevity>

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190327172701_InitialCreate', N'5.0-rtm');
Done.

Use SQL Server Object Explorer to inspect the database as you did in the first tutorial.
You'll notice the addition of an __EFMigrationsHistory table that keeps track of which
migrations have been applied to the database. View the data in that table and you'll see
one row for the first migration. (The last log in the preceding CLI output example shows
the INSERT statement that creates this row.)

Run the application to verify that everything still works the same as before.
Compare CLI and PMC
The EF tooling for managing migrations is available from .NET Core CLI commands or
from PowerShell cmdlets in the Visual Studio Package Manager Console (PMC) window.
This tutorial shows how to use the CLI, but you can use the PMC if you prefer.

The EF commands for the PMC commands are in the


Microsoft.EntityFrameworkCore.Tools package. This package is included in the
Microsoft.AspNetCore.App metapackage, so you don't need to add a package reference
if your app has a package reference for Microsoft.AspNetCore.App .

Important: This isn't the same package as the one you install for the CLI by editing the
.csproj file. The name of this one ends in Tools , unlike the CLI package name which

ends in Tools.DotNet .

For more information about the CLI commands, see .NET Core CLI.

For more information about the PMC commands, see Package Manager Console (Visual
Studio).

Get the code


Download or view the completed application.

Next step
Advance to the next tutorial to begin looking at more advanced topics about expanding
the data model. Along the way you'll create and apply additional migrations.

Create and apply additional migrations


Tutorial: Create a complex data model -
ASP.NET MVC with EF Core
Article • 04/11/2023

In the previous tutorials, you worked with a simple data model that was composed of
three entities. In this tutorial, you'll add more entities and relationships and you'll
customize the data model by specifying formatting, validation, and database mapping
rules.

When you're finished, the entity classes will make up the completed data model that's
shown in the following illustration:
In this tutorial, you:

" Customize the Data model


" Make changes to Student entity
" Create Instructor entity
" Create OfficeAssignment entity
" Modify Course entity
" Create Department entity
" Modify Enrollment entity
" Update the database context
" Seed database with test data
" Add a migration
" Change the connection string
" Update the database

Prerequisites
Using EF Core migrations

Customize the Data model


In this section you'll see how to customize the data model by using attributes that
specify formatting, validation, and database mapping rules. Then in several of the
following sections you'll create the complete School data model by adding attributes to
the classes you already created and creating new classes for the remaining entity types
in the model.

The DataType attribute


For student enrollment dates, all of the web pages currently display the time along with
the date, although all you care about for this field is the date. By using data annotation
attributes, you can make one code change that will fix the display format in every view
that shows the data. To see an example of how to do that, you'll add an attribute to the
EnrollmentDate property in the Student class.

In Models/Student.cs , add a using statement for the


System.ComponentModel.DataAnnotations namespace and add DataType and

DisplayFormat attributes to the EnrollmentDate property, as shown in the following

example:

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

The DataType attribute is used to specify a data type that's more specific than the
database intrinsic type. In this case we only want to keep track of the date, not the date
and 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 attribute
emits HTML 5 data- (pronounced data dash) attributes that HTML 5 browsers can
understand. The DataType attributes don't 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:

C#

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =


true)]

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 might not 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 also. 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, some client-side input
validation, etc.).
By default, the browser will render data using the correct format based on your
locale.

For more information, see the <input> tag helper documentation.

Run the app, go to the Students Index page and notice that times are no longer
displayed for the enrollment dates. The same will be true for any view that uses the
Student model.

The StringLength attribute


You can also specify data validation rules and validation error messages using attributes.
The StringLength attribute sets the maximum length in the database and provides
client side and server side validation for ASP.NET Core MVC. You can also specify the
minimum string length in this attribute, but the minimum value has no impact on the
database schema.

Suppose you want to ensure that users don't enter more than 50 characters for a name.
To add this limitation, add StringLength attributes to the LastName and FirstMidName
properties, as shown in the following example:
C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

The StringLength attribute won't prevent a user from entering white space for a name.
You can use the RegularExpression attribute 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:

C#

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

The MaxLength attribute provides functionality similar to the StringLength attribute but
doesn't provide client side validation.

The database model has now changed in a way that requires a change in the database
schema. You'll use migrations to update the schema without losing any data that you
may have added to the database by using the application UI.

Save your changes and build the project. Then open the command window in the
project folder and enter the following commands:

.NET CLI

dotnet ef migrations add MaxLengthOnNames


.NET CLI

dotnet ef database update

The migrations add command warns that data loss may occur, because the change
makes the maximum length shorter for two columns. Migrations creates a file named
<timeStamp>_MaxLengthOnNames.cs . This file contains code in the Up method that will
update the database to match the current data model. The database update command
ran that code.

The timestamp prefixed to the migrations file name is used by Entity Framework to
order the migrations. You can create multiple migrations before running the update-
database command, and then all of the migrations are applied in the order in which they
were created.

Run the app, select the Students tab, click Create New, and try to enter either name
longer than 50 characters. The application should prevent you from doing this.

The Column attribute


You can also use attributes to control how your classes and properties are mapped to
the database. Suppose you had used the name FirstMidName for the first-name field
because the field might also contain a middle name. But you want the database column
to be named FirstName , because users who will be writing ad-hoc queries against the
database are accustomed to that name. To make this mapping, you can use the Column
attribute.

The Column attribute specifies that when the database is created, the column of the
Student table that maps to the FirstMidName property will be named FirstName . In

other words, when your code refers to Student.FirstMidName , the data will come from or
be updated in the FirstName column of the Student table. If you don't specify column
names, they're given the same name as the property name.

In the Student.cs file, add a using statement for


System.ComponentModel.DataAnnotations.Schema and add the column name attribute to

the FirstMidName property, as shown in the following highlighted code:

C#

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; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50)]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

The addition of the Column attribute changes the model backing the SchoolContext , so
it won't match the database.

Save your changes and build the project. Then open the command window in the
project folder and enter the following commands to create another migration:

.NET CLI

dotnet ef migrations add ColumnFirstName

.NET CLI

dotnet ef database update

In SQL Server Object Explorer, open the Student table designer by double-clicking the
Student table.
Before you applied the first two migrations, the name columns were of type
nvarchar(MAX). They're now nvarchar(50) and the column name has changed from
FirstMidName to FirstName.

7 Note

If you try to compile before you finish creating all of the entity classes in the
following sections, you might get compiler errors.

Changes to Student entity

In Models/Student.cs , replace the code you added earlier with the following code. The
changes are highlighted.

C#
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)]
[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;
}
}

public ICollection<Enrollment> Enrollments { get; set; }


}
}

The Required attribute


The Required attribute makes the name properties required fields. The Required
attribute isn't needed for non-nullable types such as value types (DateTime, int, double,
float, etc.). Types that can't be null are automatically treated as required fields.

The Required attribute must be used with MinimumLength for the MinimumLength to be
enforced.

C#

[Display(Name = "Last Name")]


[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

The Display attribute


The Display attribute specifies that the caption for the text boxes should be "First
Name", "Last Name", "Full Name", and "Enrollment Date" instead of the property name
in each instance (which has no space dividing the words).

The FullName calculated property


FullName is a calculated property that returns a value that's created by concatenating
two other properties. Therefore it has only a get accessor, and no FullName column will
be generated in the database.

Create Instructor entity

Create Models/Instructor.cs , replacing the template code with the following code:

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

[Display(Name = "Full Name")]


public string FullName
{
get { return LastName + ", " + FirstMidName; }
}

public ICollection<CourseAssignment> CourseAssignments { get; set; }


public OfficeAssignment OfficeAssignment { get; set; }
}
}

Notice that several properties are the same in the Student and Instructor entities. In the
Implementing Inheritance tutorial later in this series, you'll refactor this code to
eliminate the redundancy.

You can put multiple attributes on one line, so you could also write the HireDate
attributes as follows:

C#

[DataType(DataType.Date),Display(Name = "Hire
Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]

The CourseAssignments and OfficeAssignment 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.

C#
public ICollection<CourseAssignment> CourseAssignments { get; set; }

If a navigation property can hold multiple entities, its type must be a list in which entries
can be added, deleted, and updated. You can specify ICollection<T> or a type such as
List<T> or HashSet<T> . If you specify ICollection<T> , EF creates a HashSet<T>

collection by default.

The reason why these are CourseAssignment entities is explained below in the section
about many-to-many relationships.

Contoso University business rules state that an instructor can only have at most one
office, so the OfficeAssignment property holds a single OfficeAssignment entity (which
may be null if no office is assigned).

C#

public OfficeAssignment OfficeAssignment { get; set; }

Create OfficeAssignment entity

Create Models/OfficeAssignment.cs with the following code:

C#

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; }
public Instructor Instructor { get; set; }
}
}

The Key attribute


There's a one-to-zero-or-one relationship between the Instructor and the
OfficeAssignment entities. An office assignment only exists in relation to the instructor

it's assigned to, and therefore its primary key is also its foreign key to the Instructor
entity. But the Entity Framework can't automatically recognize InstructorID as the
primary key of this entity because its name doesn't follow the ID or classnameID
naming convention. Therefore, the Key attribute is used to identify it as the key:

C#

[Key]
public int InstructorID { get; set; }

You can also use the Key attribute if the entity does have its own primary key but you
want to name the property something other than classnameID or ID.

By default, EF treats the key as non-database-generated because the column is for an


identifying relationship.

The Instructor navigation property


The Instructor entity has a nullable OfficeAssignment navigation property (because an
instructor might not have an office assignment), and the OfficeAssignment entity has a
non-nullable Instructor navigation property (because an office assignment can't exist
without an instructor -- InstructorID is non-nullable). When an Instructor entity has a
related OfficeAssignment entity, each entity will have a reference to the other one in its
navigation property.

You could put a [Required] attribute on the Instructor navigation property to specify
that there must be a related instructor, but you don't have to do that because the
InstructorID foreign key (which is also the key to this table) is non-nullable.

Modify Course entity


In Models/Course.cs , replace the code you added earlier with the following code. The
changes are highlighted.

C#

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

[StringLength(50, MinimumLength = 3)]


public string Title { get; set; }

[Range(0, 5)]
public int Credits { get; set; }

public int DepartmentID { get; set; }

public Department Department { get; set; }


public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}

The course entity has a foreign key property DepartmentID which points to the related
Department entity and it has a Department navigation property.

The Entity Framework doesn't require you to add a foreign key property to your data
model when you have a navigation property for a related entity. EF automatically creates
foreign keys in the database wherever they're needed and creates shadow properties for
them. But having the foreign key in the data model can make updates simpler and more
efficient. For example, when you fetch a Course entity to edit, the Department entity is
null if you don't load it, so when you update the Course entity, you would have to first
fetch the Department entity. When the foreign key property DepartmentID is included in
the data model, you don't need to fetch the Department entity before you update.

The DatabaseGenerated attribute


The DatabaseGenerated attribute with the None parameter on the CourseID property
specifies that primary key values are provided by the user rather than generated by the
database.

C#

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

By default, Entity Framework assumes that primary key values are generated by the
database. That's what you want in most scenarios. However, for Course entities, you'll
use a user-specified course number such as a 1000 series for one department, a 2000
series for another department, and so on.

The DatabaseGenerated attribute can also be used to generate default values, as in the
case of database columns used to record the date a row was created or updated. For
more information, see Generated Properties.

Foreign key and navigation properties


The foreign key properties and navigation properties in the Course entity reflect the
following relationships:

A course is assigned to one department, so there's a DepartmentID foreign key and a


Department navigation property for the reasons mentioned above.

C#

public int DepartmentID { get; set; }


public Department Department { get; set; }

A course can have any number of students enrolled in it, so the Enrollments navigation
property is a collection:
C#

public ICollection<Enrollment> Enrollments { get; set; }

A course may be taught by multiple instructors, so the CourseAssignments navigation


property is a collection (the type CourseAssignment is explained later):

C#

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Create Department entity

Create Models/Department.cs with the following code:

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { 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; }

public int? InstructorID { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}

The Column attribute


Earlier you used the Column attribute to change column name mapping. In the code for
the Department entity, the Column attribute is being used to change SQL data type
mapping so that the column will be defined using the SQL Server money type in the
database:

C#

[Column(TypeName="money")]
public decimal Budget { get; set; }

Column mapping is generally not required, because the Entity Framework chooses the
appropriate SQL Server data type based on the CLR type that you define for the
property. The CLR decimal type maps to a SQL Server decimal type. But in this case you
know that the column will be holding currency amounts, and the money data type is
more appropriate for that.

Foreign key and navigation properties


The foreign key and navigation properties reflect the following relationships:

A department may or may not have an administrator, and an administrator is always an


instructor. Therefore the InstructorID property is included as the foreign key to the
Instructor entity, and a question mark is added after the int type designation to mark
the property as nullable. The navigation property is named Administrator but holds an
Instructor entity:

C#
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

A department may have many courses, so there's a Courses navigation property:

C#

public ICollection<Course> Courses { get; set; }

7 Note

By convention, the Entity Framework enables cascade delete for non-nullable


foreign keys and for many-to-many relationships. This can result in circular cascade
delete rules, which will cause an exception when you try to add a migration. For
example, if you didn't define the Department.InstructorID property as nullable, EF
would configure a cascade delete rule to delete the department when you delete
the instructor, which isn't what you want to have happen. If your business rules
required the InstructorID property to be non-nullable, you would have to use the
following fluent API statement to disable cascade delete on the relationship:

C#

modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)

Modify Enrollment entity

In Models/Enrollment.cs , replace the code you added earlier with the following code:
C#

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}

public class Enrollment


{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }

public Course Course { get; set; }


public Student Student { get; set; }
}
}

Foreign key and navigation properties


The foreign key properties and navigation properties reflect the following relationships:

An enrollment record is for a single course, so there's a CourseID foreign key property
and a Course navigation property:

C#

public int CourseID { get; set; }


public Course Course { get; set; }

An enrollment record is for a single student, so there's a StudentID foreign key property
and a Student navigation property:

C#

public int StudentID { get; set; }


public Student Student { get; set; }

Many-to-Many relationships
There's a many-to-many relationship between the Student and Course entities, and 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
foreign keys for the joined tables (in this case, a primary key and a Grade property).

The following illustration shows what these relationships look like in an entity diagram.
(This diagram was generated using the Entity Framework Power Tools for EF 6.x; creating
the diagram isn't part of the tutorial, it's just being used here as an illustration.)

Each relationship line has a 1 at one end and an asterisk (*) at the other, indicating a
one-to-many relationship.

If the Enrollment table didn't include grade information, it would only need to contain
the two foreign keys CourseID and StudentID . In that case, it would be a many-to-many
join table without payload (or a pure join table) in the database. The Instructor and
Course entities have that kind of many-to-many relationship, and your next step is to
create an entity class to function as a join table without payload.

EF Core supports implicit join tables for many-to-many relationships, but this tutoral has
not been updated to use an implicit join table. See Many-to-Many Relationships, the
Razor Pages version of this tutorial which has been updated.

The CourseAssignment entity

Create Models/CourseAssignment.cs with the following code:

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

Join entity names


A join table is required in the database for the Instructor-to-Courses many-to-many
relationship, and it has to be represented by an entity set. It's common to name a join
entity EntityName1EntityName2 , which in this case would be CourseInstructor . However,
we recommend that you choose a name that describes the relationship. Data models
start out simple and grow, with no-payload joins frequently getting payloads later. If
you start with a descriptive entity name, you won't have to change the name later.
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 through Ratings.
For this relationship, CourseAssignment is a better choice than CourseInstructor .
Composite key
Since the foreign keys are not nullable and together uniquely identify each row of the
table, there's no need for a separate primary key. The InstructorID and CourseID
properties should function as a composite primary key. The only way to identify
composite primary keys to EF is by using the fluent API (it can't be done by using
attributes). You'll see how to configure the composite primary key in the next section.

The composite key ensures that while you can have multiple rows for one course, and
multiple rows for one instructor, you can't have multiple rows for the same instructor
and course. The Enrollment join entity defines its own primary key, so duplicates of this
sort are possible. To prevent such duplicates, you could add a unique index on the
foreign key fields, or configure Enrollment with a primary composite key similar to
CourseAssignment . For more information, see Indexes.

Update the database context


Add the following highlighted code to the Data/SchoolContext.cs file:

C#

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}

public DbSet<Course> Courses { get; set; }


public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<CourseAssignment> CourseAssignments { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>
().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>
().ToTable("CourseAssignment");

modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}

This code adds the new entities and configures the CourseAssignment entity's
composite primary key.

About a fluent API alternative


The code in the OnModelCreating method of the DbContext class uses the fluent API to
configure EF behavior. The API is called "fluent" because it's often used by stringing a
series of method calls together into a single statement, as in this example from the EF
Core documentation:

C#

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}

In this tutorial, you're using the fluent API only for database mapping that you can't do
with attributes. However, you can also use the fluent API to specify most of the
formatting, validation, and mapping rules that you can do by using attributes. Some
attributes such as MinimumLength can't be applied with the fluent API. As mentioned
previously, MinimumLength doesn't change the schema, it only applies a client and server
side validation rule.

Some developers prefer to use the fluent API exclusively so that they can keep their
entity classes "clean." You can mix attributes and fluent API if you want, and there are a
few customizations that can only be done by using fluent API, but in general the
recommended practice is to choose one of these two approaches and use that
consistently as much as possible. If you do use both, note that wherever there's a
conflict, Fluent API overrides attributes.
For more information about attributes vs. fluent API, see Methods of configuration.

Entity Diagram Showing Relationships


The following illustration shows the diagram that the Entity Framework Power Tools
create for the completed School model.

Besides the one-to-many relationship lines (1 to *), you can see here the one-to-zero-
or-one relationship line (1 to 0..1) between the Instructor and OfficeAssignment
entities and the zero-or-one-to-many relationship line (0..1 to *) between the Instructor
and Department entities.

Seed database with test data


Replace the code in the Data/DbInitializer.cs file with the following code in order to
provide seed data for the new entities you've created.

C#

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
}

var students = new Student[]


{
new Student { FirstMidName = "Carson", LastName =
"Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName =
"Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Gytis", LastName =
"Barzdukas",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Peggy", LastName =
"Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura", LastName =
"Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino", LastName =
"Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};

foreach (Student s in students)


{
context.Students.Add(s);
}
context.SaveChanges();

var instructors = new Instructor[]


{
new Instructor { FirstMidName = "Kim", LastName =
"Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName =
"Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName =
"Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName =
"Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName =
"Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};

foreach (Instructor i in instructors)


{
context.Instructors.Add(i);
}
context.SaveChanges();

var departments = new Department[]


{
new Department { Name = "English", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName ==
"Abercrombie").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName ==
"Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName ==
"Harui").ID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName ==
"Kapoor").ID }
};
foreach (Department d in departments)
{
context.Departments.Add(d);
}
context.SaveChanges();

var courses = new Course[]


{
new Course {CourseID = 1050, Title = "Chemistry",
Credits = 3,
DepartmentID = departments.Single( s => s.Name ==
"Engineering").DepartmentID
},
new Course {CourseID = 4022, Title = "Microeconomics",
Credits = 3,
DepartmentID = departments.Single( s => s.Name ==
"Economics").DepartmentID
},
new Course {CourseID = 4041, Title = "Macroeconomics",
Credits = 3,
DepartmentID = departments.Single( s => s.Name ==
"Economics").DepartmentID
},
new Course {CourseID = 1045, Title = "Calculus",
Credits = 4,
DepartmentID = departments.Single( s => s.Name ==
"Mathematics").DepartmentID
},
new Course {CourseID = 3141, Title = "Trigonometry",
Credits = 4,
DepartmentID = departments.Single( s => s.Name ==
"Mathematics").DepartmentID
},
new Course {CourseID = 2021, Title = "Composition",
Credits = 3,
DepartmentID = departments.Single( s => s.Name ==
"English").DepartmentID
},
new Course {CourseID = 2042, Title = "Literature",
Credits = 4,
DepartmentID = departments.Single( s => s.Name ==
"English").DepartmentID
},
};

foreach (Course c in courses)


{
context.Courses.Add(c);
}
context.SaveChanges();

var officeAssignments = new OfficeAssignment[]


{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName ==
"Fakhouri").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName ==
"Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName ==
"Kapoor").ID,
Location = "Thompson 304" },
};

foreach (OfficeAssignment o in officeAssignments)


{
context.OfficeAssignments.Add(o);
}
context.SaveChanges();

var courseInstructors = new CourseAssignment[]


{
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Kapoor").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title ==
"Microeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title ==
"Macroeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Calculus"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Fakhouri").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Trigonometry"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Composition"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Abercrombie").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Literature"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Abercrombie").ID
},
};

foreach (CourseAssignment ci in courseInstructors)


{
context.CourseAssignments.Add(ci);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry"
).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alexander").ID,
CourseID = courses.Single(c => c.Title ==
"Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alexander").ID,
CourseID = courses.Single(c => c.Title ==
"Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus"
).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry"
).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition"
).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry"
).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Anand").ID,
CourseID = courses.Single(c => c.Title ==
"Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Barzdukas").ID,
CourseID = courses.Single(c => c.Title ==
"Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title ==
"Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Justice").ID,
CourseID = courses.Single(c => c.Title ==
"Literature").CourseID,
Grade = Grade.B
}
};

foreach (Enrollment e in enrollments)


{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID ==
e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
}
}

As you saw in the first tutorial, most of this code simply creates new entity objects and
loads sample data into properties as required for testing. Notice how the many-to-many
relationships are handled: the code creates relationships by creating entities in the
Enrollments and CourseAssignment join entity sets.

Add a migration
Save your changes and build the project. Then open the command window in the
project folder and enter the migrations add command (don't do the update-database
command yet):

.NET CLI

dotnet ef migrations add ComplexDataModel

You get a warning about possible data loss.

text

An operation was scaffolded that may result in the loss of data. Please
review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

If you tried to run the database update command at this point (don't do it yet), you
would get the following error:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint
"FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in database
"ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

Sometimes when you execute migrations with existing data, you need to insert stub
data into the database to satisfy foreign key constraints. The generated code in the Up
method adds a non-nullable DepartmentID foreign key to the Course table. If there are
already rows in the Course table when the code runs, the AddColumn operation fails
because SQL Server doesn't know what value to put in the column that can't be null. For
this tutorial you'll run the migration on a new database, but in a production application
you'd have to make the migration handle existing data, so the following directions show
an example of how to do that.

To make this migration work with existing data you have to change the code to give the
new column a default value, and create a stub department named "Temp" to act as the
default department. As a result, existing Course rows will all be related to the "Temp"
department after the Up method runs.

Open the {timestamp}_ComplexDataModel.cs file.

Comment out the line of code that adds the DepartmentID column to the Course
table.

C#

migrationBuilder.AlterColumn<string>(
name: "Title",
table: "Course",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldNullable: true);

//migrationBuilder.AddColumn<int>(
// name: "DepartmentID",
// table: "Course",
// nullable: false,
// defaultValue: 0);

Add the following highlighted code after the code that creates the Department
table:

C#

migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(nullable: true),
Name = table.Column<string>(maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Department", x => x.DepartmentID);
table.ForeignKey(
name: "FK_Department_Instructor_InstructorID",
column: x => x.InstructorID,
principalTable: "Instructor",
principalColumn: "ID",
onDelete: ReferentialAction.Restrict);
});

migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget,


StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
nullable: false,
defaultValue: 1);

In a production application, you would write code or scripts to add Department rows
and relate Course rows to the new Department rows. You would then no longer need
the "Temp" department or the default value on the Course.DepartmentID column.

Save your changes and build the project.

Change the connection string


You now have new code in the DbInitializer class that adds seed data for the new
entities to an empty database. To make EF create a new empty database, change the
name of the database in the connection string in appsettings.json to
ContosoUniversity3 or some other name that you haven't used on the computer you're
using.

JSON

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;
MultipleActiveResultSets=true"
},

Save your change to appsettings.json .


7 Note

As an alternative to changing the database name, you can delete the database. Use
SQL Server Object Explorer (SSOX) or the database drop CLI command:

.NET CLI

dotnet ef database drop

Update the database


After you have changed the database name or deleted the database, run the database
update command in the command window to execute the migrations.

.NET CLI

dotnet ef database update

Run the app to cause the DbInitializer.Initialize method to run and populate the
new database.

Open the database in SSOX as you did earlier, and expand the Tables node to see that
all of the tables have been created. (If you still have SSOX open from the earlier time,
click the Refresh button.)
Run the app to trigger the initializer code that seeds the database.

Right-click the CourseAssignment table and select View Data to verify that it has data in
it.

Get the code


Download or view the completed application.

Next steps
In this tutorial, you:

" Customized the Data model


" Made changes to Student entity
" Created Instructor entity
" Created OfficeAssignment entity
" Modified Course entity
" Created Department entity
" Modified Enrollment entity
" Updated the database context
" Seeded database with test data
" Added a migration
" Changed the connection string
" Updated the database

Advance to the next tutorial to learn more about how to access related data.
Next: Access related data
Tutorial: Read related data - ASP.NET
MVC with EF Core
Article • 03/28/2023

In the previous tutorial, you completed the School data model. In this tutorial, you'll
read and display related data -- that is, data that the Entity Framework loads into
navigation properties.

The following illustrations show the pages that you'll work with.
In this tutorial, you:

" Learn how to load related data


" Create a Courses page
" Create an Instructors page
" Learn about explicit loading
Prerequisites
Create a complex data model

Learn how to load related data


There are several ways that Object-Relational Mapping (ORM) software such as Entity
Framework can load related data into the navigation properties of an entity:

Eager loading: When the entity is read, related data is retrieved along with it. This
typically results in a single join query that retrieves all of the data that's needed.
You specify eager loading in Entity Framework Core by using the Include and
ThenInclude methods.

You can retrieve some of the data in separate queries, and EF "fixes up" the
navigation properties. That is, EF automatically adds the separately retrieved
entities where they belong in navigation properties of previously retrieved entities.
For the query that retrieves related data, you can use the Load method instead of a
method that returns a list or object, such as ToList or Single .

Explicit loading: When the entity is first read, related data isn't retrieved. You write
code that retrieves the related data if it's needed. As in the case of eager loading
with separate queries, explicit loading results in multiple queries sent to the
database. The difference is that with explicit loading, the code specifies the
navigation properties to be loaded. In Entity Framework Core 1.1 you can use the
Load method to do explicit loading. For example:
Lazy loading: When the entity is first read, related data isn't retrieved. However, the
first time you attempt to access a navigation property, the data required for that
navigation property is automatically retrieved. A query is sent to the database each
time you try to get data from a navigation property for the first time. Entity
Framework Core 1.0 doesn't support lazy loading.

Performance considerations
If you know you need related data for every entity retrieved, eager loading often offers
the best performance, because a single query sent to the database is typically more
efficient than separate queries for each entity retrieved. For example, suppose that each
department has ten related courses. Eager loading of all related data would result in just
a single (join) query and a single round trip to the database. A separate query for
courses for each department would result in eleven round trips to the database. The
extra round trips to the database are especially detrimental to performance when
latency is high.

On the other hand, in some scenarios separate queries is more efficient. Eager loading
of all related data in one query might cause a very complex join to be generated, which
SQL Server can't process efficiently. Or if you need to access an entity's navigation
properties only for a subset of a set of the entities you're processing, separate queries
might perform better because eager loading of everything up front would retrieve more
data than you need. If performance is critical, it's best to test performance both ways in
order to make the best choice.

Create a Courses page


The Course entity includes a navigation property that contains the Department entity of
the department that the course is assigned to. To display the name of the assigned
department in a list of courses, you need to get the Name property from the Department
entity that's in the Course.Department navigation property.

Create a controller named CoursesController for the Course entity type, using the same
options for the MVC Controller with views, using Entity Framework scaffolder that you
did earlier for the StudentsController , as shown in the following illustration:

Open CoursesController.cs and examine the Index method. The automatic scaffolding
has specified eager loading for the Department navigation property by using the
Include method.

Replace the Index method with the following code that uses a more appropriate name
for the IQueryable that returns Course entities ( courses instead of schoolContext ):

C#

public async Task<IActionResult> Index()


{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}

Open Views/Courses/Index.cshtml and replace the template code with the following
code. The changes are highlighted:

CSHTML

@model IEnumerable<ContosoUniversity.Models.Course>

@{
ViewData["Title"] = "Courses";
}
<h2>Courses</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
<a asp-action="Edit" asp-route-
id="@item.CourseID">Edit</a> |
<a asp-action="Details" asp-route-
id="@item.CourseID">Details</a> |
<a asp-action="Delete" asp-route-
id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

You've made the following changes to the scaffolded code:


Changed the heading from Index to Courses.

Added a Number column that shows the CourseID property value. By default,
primary keys aren't scaffolded because normally they're meaningless to end users.
However, in this case the primary key is meaningful and you want to show it.

Changed the Department column to display the department name. The code
displays the Name property of the Department entity that's loaded into the
Department navigation property:

HTML

@Html.DisplayFor(modelItem => item.Department.Name)

Run the app and select the Courses tab to see the list with department names.

Create an Instructors page


In this section, you'll create a controller and view for the Instructor entity in order to
display the Instructors page:
This page reads and displays related data in the following ways:

The list of instructors displays related data from the OfficeAssignment entity. The
Instructor and OfficeAssignment entities are in a one-to-zero-or-one

relationship. You'll use eager loading for the OfficeAssignment entities. As


explained earlier, eager loading is typically more efficient when you need the
related data for all retrieved rows of the primary table. In this case, you want to
display office assignments for all displayed instructors.

When the user selects an instructor, related Course entities are displayed. The
Instructor and Course entities are in a many-to-many relationship. You'll use
eager loading for the Course entities and their related Department entities. In this
case, separate queries might be more efficient because you need courses only for
the selected instructor. However, this example shows how to use eager loading for
navigation properties within entities that are themselves in navigation properties.

When the user selects a course, related data from the Enrollments entity set is
displayed. The Course and Enrollment entities are in a one-to-many relationship.
You'll use separate queries for Enrollment entities and their related Student
entities.

Create a view model for the Instructor Index view


The Instructors page shows data from three different tables. Therefore, you'll create a
view model that includes three properties, each holding the data for one of the tables.

In the SchoolViewModels folder, create InstructorIndexData.cs and replace the existing


code with the following code:

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}

Create the Instructor controller and views


Create an Instructors controller with EF read/write actions as shown in the following
illustration:
Open InstructorsController.cs and add a using statement for the ViewModels
namespace:

C#

using ContosoUniversity.Models.SchoolViewModels;

Replace the Index method with the following code to do eager loading of related data
and put it in the view model.

C#

public async Task<IActionResult> Index(int? id, int? courseID)


{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s =>
s.Course);
}

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}

return View(viewModel);
}

The method accepts optional route data ( id ) and a query string parameter ( courseID )
that provide the ID values of the selected instructor and selected course. The parameters
are provided by the Select hyperlinks on the page.

The code begins by creating an instance of the view model and putting in it the list of
instructors. The code specifies eager loading for the Instructor.OfficeAssignment and
the Instructor.CourseAssignments navigation properties. Within the CourseAssignments
property, the Course property is loaded, and within that, the Enrollments and
Department properties are loaded, and within each Enrollment entity the Student

property is loaded.

C#

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

Since the view always requires the OfficeAssignment entity, it's more efficient to fetch
that in the same query. Course entities are required when an instructor is selected in the
web page, so a single query is better than multiple queries only if the page is displayed
more often with a course selected than without.
The code repeats CourseAssignments and Course because you need two properties from
Course . The first string of ThenInclude calls gets CourseAssignment.Course ,
Course.Enrollments , and Enrollment.Student .

You can read more about including multiple levels of related data here.

C#

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

At that point in the code, another ThenInclude would be for navigation properties of
Student , which you don't need. But calling Include starts over with Instructor
properties, so you have to go through the chain again, this time specifying
Course.Department instead of Course.Enrollments .

C#

viewModel.Instructors = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();

The following code executes when an instructor was selected. The selected instructor is
retrieved from the list of instructors in the view model. The view model's Courses
property is then loaded with the Course entities from that instructor's
CourseAssignments navigation property.

C#
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

The Where method returns a collection, but in this case the criteria passed to that
method result in only a single Instructor entity being returned. The Single method
converts the collection into a single Instructor entity, which gives you access to that
entity's CourseAssignments property. The CourseAssignments property contains
CourseAssignment entities, from which you want only the related Course entities.

You use the Single method on a collection when you know the collection will have only
one item. The Single method throws an exception if the collection passed to it's empty
or if there's more than one item. An alternative is SingleOrDefault , which returns a
default value (null in this case) if the collection is empty. However, in this case that
would still result in an exception (from trying to find a Courses property on a null
reference), and the exception message would less clearly indicate the cause of the
problem. When you call the Single method, you can also pass in the Where condition
instead of calling the Where method separately:

C#

.Single(i => i.ID == id.Value)

Instead of:

C#

.Where(i => i.ID == id.Value).Single()

Next, if a course was selected, the selected course is retrieved from the list of courses in
the view model. Then the view model's Enrollments property is loaded with the
Enrollment entities from that course's Enrollments navigation property.

C#

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}

Tracking vs no-tracking
No-tracking queries are useful when the results are used in a read-only scenario. They're
generally quicker to execute because there's no need to set up the change tracking
information. If the entities retrieved from the database don't need to be updated, then a
no-tracking query is likely to perform better than a tracking query.

In some cases a tracking query is more efficient than a no-tracking query. For more
information, see Tracking vs. No-Tracking Queries.

Modify the Instructor Index view


In Views/Instructors/Index.cshtml , replace the template code with the following code.
The changes are highlighted.

CSHTML

@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData

@{
ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @course.Course.Title <br />
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a>
|
<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>

You've made the following changes to the existing code:

Changed the model class to InstructorIndexData .

Changed the page title from Index to Instructors.

Added an Office column that displays item.OfficeAssignment.Location only if


item.OfficeAssignment isn't null. (Because this is a one-to-zero-or-one
relationship, there might not be a related OfficeAssignment entity.)

CSHTML
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}

Added a Courses column that displays courses taught by each instructor. For more
information, see the Explicit line transition section of the Razor syntax article.

Added code that conditionally adds a Bootstrap CSS class to the tr element of the
selected instructor. This class sets a background color for the selected row.

Added a new hyperlink labeled Select immediately before the other links in each
row, which causes the selected instructor's ID to be sent to the Index method.

CSHTML

<a asp-action="Index" asp-route-id="@item.ID">Select</a> |

Run the app and select the Instructors tab. The page displays the Location property of
related OfficeAssignment entities and an empty table cell when there's no related
OfficeAssignment entity.

In the Views/Instructors/Index.cshtml file, after the closing table element (at the end of
the file), add the following code. This code displays a list of courses related to an
instructor when an instructor is selected.
CSHTML

@if (Model.Courses != null)


{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>

@foreach (var item in Model.Courses)


{
string selectedRow = "";
if (item.CourseID == (int?)ViewData["CourseID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "Index", new { courseID =
item.CourseID })
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}

</table>
}

This code reads the Courses property of the view model to display a list of courses. It
also provides a Select hyperlink that sends the ID of the selected course to the Index
action method.

Refresh the page and select an instructor. Now you see a grid that displays courses
assigned to the selected instructor, and for each course you see the name of the
assigned department.
After the code block you just added, add the following code. This displays a list of the
students who are enrolled in a course when that course is selected.

CSHTML

@if (Model.Enrollments != null)


{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}

This code reads the Enrollments property of the view model in order to display a list of
students enrolled in the course.

Refresh the page again and select an instructor. Then select a course to see the list of
enrolled students and their grades.
About explicit loading
When you retrieved the list of instructors in InstructorsController.cs , you specified
eager loading for the CourseAssignments navigation property.

Suppose you expected users to only rarely want to see enrollments in a selected
instructor and course. In that case, you might want to load the enrollment data only if
it's requested. To see an example of how to do explicit loading, replace the Index
method with the following code, which removes eager loading for Enrollments and
loads that property explicitly. The code changes are highlighted.

C#

public async Task<IActionResult> Index(int? id, int? courseID)


{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();

if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s =>
s.Course);
}

if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
var selectedCourse = viewModel.Courses.Where(x => x.CourseID ==
courseID).Single();
await _context.Entry(selectedCourse).Collection(x =>
x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x =>
x.Student).LoadAsync();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}

return View(viewModel);
}

The new code drops the ThenInclude method calls for enrollment data from the code
that retrieves instructor entities. It also drops AsNoTracking . If an instructor and course
are selected, the highlighted code retrieves Enrollment entities for the selected course,
and Student entities for each Enrollment .
Run the app, go to the Instructors Index page now and you'll see no difference in what's
displayed on the page, although you've changed how the data is retrieved.

Get the code


Download or view the completed application.

Next steps
In this tutorial, you:

" Learned how to load related data


" Created a Courses page
" Created an Instructors page
" Learned about explicit loading

Advance to the next tutorial to learn how to update related data.

Update related data


Tutorial: Update related data - ASP.NET
MVC with EF Core
Article • 04/06/2023

In the previous tutorial you displayed related data; in this tutorial you'll update related
data by updating foreign key fields and navigation properties.

The following illustrations show some of the pages that you'll work with.
In this tutorial, you:

" Customize Courses pages


" Add Instructors Edit page
" Add courses to Edit page
" Update Delete page
" Add office location and courses to Create page

Prerequisites
Read related data
Customize Courses pages
When a new Course entity is created, it must have a relationship to an existing
department. To facilitate this, the scaffolded code includes controller methods and
Create and Edit views that include a drop-down list for selecting the department. The
drop-down list sets the Course.DepartmentID foreign key property, and that's all the
Entity Framework needs in order to load the Department navigation property with the
appropriate Department entity. You'll use the scaffolded code, but change it slightly to
add error handling and sort the drop-down list.

In CoursesController.cs , delete the four Create and Edit methods and replace them
with the following code:

C#

public IActionResult Create()


{
PopulateDepartmentsDropDownList();
return View();
}

C#

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult>
Create([Bind("CourseID,Credits,DepartmentID,Title")] Course course)
{
if (ModelState.IsValid)
{
_context.Add(course);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}

C#

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}
var course = await _context.Courses
.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}

C#

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}

var courseToUpdate = await _context.Courses


.FirstOrDefaultAsync(c => c.CourseID == id);

if (await TryUpdateModelAsync<Course>(courseToUpdate,
"",
c => c.Credits, c => c.DepartmentID, c => c.Title))
{
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
return View(courseToUpdate);
}

After the Edit HttpPost method, create a new method that loads department info for
the drop-down list.

C#
private void PopulateDepartmentsDropDownList(object selectedDepartment =
null)
{
var departmentsQuery = from d in _context.Departments
orderby d.Name
select d;
ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(),
"DepartmentID", "Name", selectedDepartment);
}

The PopulateDepartmentsDropDownList method gets a list of all departments sorted by


name, creates a SelectList collection for a drop-down list, and passes the collection to
the view in ViewBag . The method accepts the optional selectedDepartment parameter
that allows the calling code to specify the item that will be selected when the drop-
down list is rendered. The view will pass the name "DepartmentID" to the <select> tag
helper, and the helper then knows to look in the ViewBag object for a SelectList
named "DepartmentID".

The HttpGet Create method calls the PopulateDepartmentsDropDownList method without


setting the selected item, because for a new course the department isn't established yet:

C#

public IActionResult Create()


{
PopulateDepartmentsDropDownList();
return View();
}

The HttpGet Edit method sets the selected item, based on the ID of the department
that's already assigned to the course being edited:

C#

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}

The HttpPost methods for both Create and Edit also include code that sets the
selected item when they redisplay the page after an error. This ensures that when the
page is redisplayed to show the error message, whatever department was selected stays
selected.

Add .AsNoTracking to Details and Delete methods


To optimize performance of the Course Details and Delete pages, add AsNoTracking
calls in the Details and HttpGet Delete methods.

C#

public async Task<IActionResult> Details(int? id)


{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.Include(c => c.Department)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}

return View(course);
}

C#

public async Task<IActionResult> Delete(int? id)


{
if (id == null)
{
return NotFound();
}

var course = await _context.Courses


.Include(c => c.Department)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}

return View(course);
}

Modify the Course views


In Views/Courses/Create.cshtml , add a "Select Department" option to the Department
drop-down list, change the caption from DepartmentID to Department, and add a
validation message.

CSHTML

<div class="form-group">
<label asp-for="Department" class="control-label"></label>
<select asp-for="DepartmentID" class="form-control" asp-
items="ViewBag.DepartmentID">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="DepartmentID" class="text-danger" />
</div>

In Views/Courses/Edit.cshtml , make the same change for the Department field that you
just did in Create.cshtml .

Also in Views/Courses/Edit.cshtml , add a course number field before the Title field.
Because the course number is the primary key, it's displayed, but it can't be changed.

CSHTML

<div class="form-group">
<label asp-for="CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.CourseID)</div>
</div>

There's already a hidden field ( <input type="hidden"> ) for the course number in the Edit
view. Adding a <label> tag helper doesn't eliminate the need for the hidden field
because it doesn't cause the course number to be included in the posted data when the
user clicks Save on the Edit page.

In Views/Courses/Delete.cshtml , add a course number field at the top and change


department ID to department name.
CSHTML

@model ContosoUniversity.Models.Course

@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>


<div>
<h4>Course</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.CourseID)
</dd>
<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.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Name)
</dd>
</dl>

<form asp-action="Delete">
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>

In Views/Courses/Details.cshtml , make the same change that you just did for
Delete.cshtml .
Test the Course pages
Run the app, select the Courses tab, click Create New, and enter data for a new course:

Click Create. The Courses Index page is displayed with the new course added to the list.
The department name in the Index page list comes from the navigation property,
showing that the relationship was established correctly.

Click Edit on a course in the Courses Index page.


Change data on the page and click Save. The Courses Index page is displayed with the
updated course data.

Add Instructors Edit page


When you edit an instructor record, you want to be able to update the instructor's office
assignment. The Instructor entity has a one-to-zero-or-one relationship with the
OfficeAssignment entity, which means your code has to handle the following situations:

If the user clears the office assignment and it originally had a value, delete the
OfficeAssignment entity.

If the user enters an office assignment value and it originally was empty, create a
new OfficeAssignment entity.

If the user changes the value of an office assignment, change the value in an
existing OfficeAssignment entity.
Update the Instructors controller
In InstructorsController.cs , change the code in the HttpGet Edit method so that it
loads the Instructor entity's OfficeAssignment navigation property and calls
AsNoTracking :

C#

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (instructor == null)
{
return NotFound();
}
return View(instructor);
}

Replace the HttpPost Edit method with the following code to handle office assignment
updates:

C#

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.FirstOrDefaultAsync(s => s.ID == id);

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i =>
i.OfficeAssignment))
{
if
(String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
return View(instructorToUpdate);
}

The code does the following:

Changes the method name to EditPost because the signature is now the same as
the HttpGet Edit method (the ActionName attribute specifies that the /Edit/ URL
is still used).

Gets the current Instructor entity from the database using eager loading for the
OfficeAssignment navigation property. This is the same as what you did in the
HttpGet Edit method.

Updates the retrieved Instructor entity with values from the model binder. The
TryUpdateModel overload enables you to declare the properties you want to

include. This prevents over-posting, as explained in the second tutorial.

C#

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i =>
i.OfficeAssignment))

If the office location is blank, sets the Instructor.OfficeAssignment property to


null so that the related row in the OfficeAssignment table will be deleted.

C#
if
(String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Locatio
n))
{
instructorToUpdate.OfficeAssignment = null;
}

Saves the changes to the database.

Update the Instructor Edit view


In Views/Instructors/Edit.cshtml , add a new field for editing the office location, at the
end before the Save button:

CSHTML

<div class="form-group">
<label asp-for="OfficeAssignment.Location" class="control-label">
</label>
<input asp-for="OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="OfficeAssignment.Location" class="text-danger"
/>
</div>

Run the app, select the Instructors tab, and then click Edit on an instructor. Change the
Office Location and click Save.
Add courses to Edit page
Instructors may teach any number of courses. Now you'll enhance the Instructor Edit
page by adding the ability to change course assignments using a group of checkboxes,
as shown in the following screen shot:
The relationship between the Course and Instructor entities is many-to-many. To add
and remove relationships, you add and remove entities to and from the
CourseAssignments join entity set.

The UI that enables you to change which courses an instructor is assigned to is a group
of checkboxes. A checkbox for every course in the database is displayed, and the ones
that the instructor is currently assigned to are selected. The user can select or clear
checkboxes to change course assignments. If the number of courses were much greater,
you would probably want to use a different method of presenting the data in the view,
but you'd use the same method of manipulating a join entity to create or delete
relationships.
Update the Instructors controller
To provide data to the view for the list of checkboxes, you'll use a view model class.

Create AssignedCourseData.cs in the SchoolViewModels folder and replace the existing


code with the following code:

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}

In InstructorsController.cs , replace the HttpGet Edit method with the following code.
The changes are highlighted.

C#

public async Task<IActionResult> Edit(int? id)


{
if (id == null)
{
return NotFound();
}

var instructor = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)


{
var allCourses = _context.Courses;
var instructorCourses = new HashSet<int>
(instructor.CourseAssignments.Select(c => c.CourseID));
var viewModel = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewData["Courses"] = viewModel;
}

The code adds eager loading for the Courses navigation property and calls the new
PopulateAssignedCourseData method to provide information for the checkbox array

using the AssignedCourseData view model class.

The code in the PopulateAssignedCourseData method reads through all Course entities
in order to load a list of courses using the view model class. For each course, the code
checks whether the course exists in the instructor's Courses navigation property. To
create efficient lookup when checking whether a course is assigned to the instructor, the
courses assigned to the instructor are put into a HashSet collection. The Assigned
property is set to true for courses the instructor is assigned to. The view will use this
property to determine which checkboxes must be displayed as selected. Finally, the list
is passed to the view in ViewData .

Next, add the code that's executed when the user clicks Save. Replace the EditPost
method with the following code, and add a new method that updates the Courses
navigation property of the Instructor entity.

C#

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, string[] selectedCourses)
{
if (id == null)
{
return NotFound();
}

var instructorToUpdate = await _context.Instructors


.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.FirstOrDefaultAsync(m => m.ID == id);

if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i =>
i.OfficeAssignment))
{
if
(String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
PopulateAssignedCourseData(instructorToUpdate);
return View(instructorToUpdate);
}

C#
private void UpdateInstructorCourses(string[] selectedCourses, Instructor
instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c =>
c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new
CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID =
course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

The method signature is now different from the HttpGet Edit method, so the method
name changes from EditPost back to Edit .

Since the view doesn't have a collection of Course entities, the model binder can't
automatically update the CourseAssignments navigation property. Instead of using the
model binder to update the CourseAssignments navigation property, you do that in the
new UpdateInstructorCourses method. Therefore, you need to exclude the
CourseAssignments property from model binding. This doesn't require any change to the

code that calls TryUpdateModel because you're using the overload that requires explicit
approval and CourseAssignments isn't in the include list.
If no checkboxes were selected, the code in UpdateInstructorCourses initializes the
CourseAssignments navigation property with an empty collection and returns:

C#

private void UpdateInstructorCourses(string[] selectedCourses, Instructor


instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c =>
c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new
CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID =
course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

The code then loops through all courses in the database and checks each course against
the ones currently assigned to the instructor versus the ones that were selected in the
view. To facilitate efficient lookups, the latter two collections are stored in HashSet
objects.

If the checkbox for a course was selected but the course isn't in the
Instructor.CourseAssignments navigation property, the course is added to the collection
in the navigation property.

C#

private void UpdateInstructorCourses(string[] selectedCourses, Instructor


instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c =>
c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new
CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID =
course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

If the checkbox for a course wasn't selected, but the course is in the
Instructor.CourseAssignments navigation property, the course is removed from the
navigation property.

C#

private void UpdateInstructorCourses(string[] selectedCourses, Instructor


instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);


var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c =>
c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new
CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID =
course.CourseID });
}
}
else
{

if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}

Update the Instructor views


In Views/Instructors/Edit.cshtml , add a Courses field with an array of checkboxes by
adding the following code immediately after the div elements for the Office field and
before the div element for the Save button.

7 Note

When you paste the code in Visual Studio, line breaks might be changed in a way
that breaks the code. If the code looks different after pasting, press Ctrl+Z one time
to undo the automatic formatting. This will fix the line breaks so that they look like
what you see here. The indentation doesn't have to be perfect, but the @:</tr>
<tr> , @:<td> , @:</td> , and @:</tr> lines must each be on a single line as shown or

you'll get a runtime error. With the block of new code selected, press Tab three
times to line up the new code with the existing code. This problem is fixed in Visual
Studio 2019.

CSHTML

<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;

List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses =
ViewBag.Courses;

foreach (var course in courses)


{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ?
"checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>

This code creates an HTML table that has three columns. In each column is a checkbox
followed by a caption that consists of the course number and title. The checkboxes all
have the same name ("selectedCourses"), which informs the model binder that they're to
be treated as a group. The value attribute of each checkbox is set to the value of
CourseID . When the page is posted, the model binder passes an array to the controller

that consists of the CourseID values for only the checkboxes which are selected.

When the checkboxes are initially rendered, those that are for courses assigned to the
instructor have checked attributes, which selects them (displays them checked).

Run the app, select the Instructors tab, and click Edit on an instructor to see the Edit
page.
Change some course assignments and click Save. The changes you make are reflected
on the Index page.

7 Note

The approach taken here to edit instructor course data works well when there's a
limited number of courses. For collections that are much larger, a different UI and a
different updating method would be required.

Update Delete page


In InstructorsController.cs , delete the DeleteConfirmed method and insert the
following code in its place.

C#

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
Instructor instructor = await _context.Instructors
.Include(i => i.CourseAssignments)
.SingleAsync(i => i.ID == id);

var departments = await _context.Departments


.Where(d => d.InstructorID == id)
.ToListAsync();
departments.ForEach(d => d.InstructorID = null);

_context.Instructors.Remove(instructor);

await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

This code makes the following changes:

Does eager loading for the CourseAssignments navigation property. You have to
include this or EF won't know about related CourseAssignment entities and won't
delete them. To avoid needing to read them here you could configure cascade
delete in the database.

If the instructor to be deleted is assigned as administrator of any departments,


removes the instructor assignment from those departments.

Add office location and courses to Create page


In InstructorsController.cs , delete the HttpGet and HttpPost Create methods, and
then add the following code in their place:

C#

public IActionResult Create()


{
var instructor = new Instructor();
instructor.CourseAssignments = new List<CourseAssignment>();
PopulateAssignedCourseData(instructor);
return View();
}
// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult>
Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor
instructor, string[] selectedCourses)
{
if (selectedCourses != null)
{
instructor.CourseAssignments = new List<CourseAssignment>();
foreach (var course in selectedCourses)
{
var courseToAdd = new CourseAssignment { InstructorID =
instructor.ID, CourseID = int.Parse(course) };
instructor.CourseAssignments.Add(courseToAdd);
}
}
if (ModelState.IsValid)
{
_context.Add(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}

This code is similar to what you saw for the Edit methods except that initially no
courses are selected. The HttpGet Create method calls the PopulateAssignedCourseData
method not because there might be courses selected but in order to provide an empty
collection for the foreach loop in the view (otherwise the view code would throw a null
reference exception).

The HttpPost Create method adds each selected course to the CourseAssignments
navigation property before it checks for validation errors and adds the new instructor to
the database. Courses are added even if there are model errors so that when there are
model errors (for an example, the user keyed an invalid date), and the page is
redisplayed with an error message, any course selections that were made are
automatically restored.

Notice that in order to be able to add courses to the CourseAssignments navigation


property you have to initialize the property as an empty collection:

C#

instructor.CourseAssignments = new List<CourseAssignment>();


As an alternative to doing this in controller code, you could do it in the Instructor
model by changing the property getter to automatically create the collection if it doesn't
exist, as shown in the following example:

C#

private ICollection<CourseAssignment> _courseAssignments;


public ICollection<CourseAssignment> CourseAssignments
{
get
{
return _courseAssignments ?? (_courseAssignments = new
List<CourseAssignment>());
}
set
{
_courseAssignments = value;
}
}

If you modify the CourseAssignments property in this way, you can remove the explicit
property initialization code in the controller.

In Views/Instructor/Create.cshtml , add an office location text box and checkboxes for


courses before the Submit button. As in the case of the Edit page, fix the formatting if
Visual Studio reformats the code when you paste it.

CSHTML

<div class="form-group">
<label asp-for="OfficeAssignment.Location" class="control-label">
</label>
<input asp-for="OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="OfficeAssignment.Location" class="text-danger"
/>
</div>

<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;

List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses =
ViewBag.Courses;

foreach (var course in courses)


{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ?
"checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>

Test by running the app and creating an instructor.

Handling Transactions
As explained in the CRUD tutorial, the Entity Framework implicitly implements
transactions. For scenarios where you need more control -- for example, if you want to
include operations done outside of Entity Framework in a transaction -- see
Transactions.

Get the code


Download or view the completed application.

Next steps
In this tutorial, you:

" Customized Courses pages


" Added Instructors Edit page
" Added courses to Edit page
" Updated Delete page
" Added office location and courses to Create page

Advance to the next tutorial to learn how to handle concurrency conflicts.

Handle concurrency conflicts


Tutorial: Handle concurrency - ASP.NET
MVC with EF Core
Article • 04/11/2023

In earlier tutorials, you learned how to update data. This tutorial shows how to handle
conflicts when multiple users update the same entity at the same time.

You'll create web pages that work with the Department entity and handle concurrency
errors. The following illustrations show the Edit and Delete pages, including some
messages that are displayed if a concurrency conflict occurs.
In this tutorial, you:

" Learn about concurrency conflicts


" Add a tracking property
" Create Departments controller and views
" Update Index view
" Update Edit methods
" Update Edit view
" Test concurrency conflicts
" Update the Delete page
" Update Details and Create views

Prerequisites
Update related data

Concurrency conflicts
A concurrency conflict occurs when one user displays an entity's data in order to edit it,
and then another user updates the same entity's data before the first user's change is
written to the database. If you don't enable the detection of such conflicts, whoever
updates the database last overwrites the other user's changes. In many applications, this
risk is acceptable: if there are few users, or few updates, or if isn't really critical if some
changes are overwritten, the cost of programming for concurrency might outweigh the
benefit. In that case, you don't have to configure the application to handle concurrency
conflicts.

Pessimistic concurrency (locking)


If your application does need to prevent accidental data loss in concurrency scenarios,
one way to do that is to use database locks. This is called pessimistic concurrency. For
example, before you read a row from a database, you request a lock for read-only or for
update access. If you lock a row for update access, no other users are allowed to lock
the row either for read-only or update access, because they would get a copy of data
that's in the process of being changed. If you lock a row for read-only access, others can
also lock it for read-only access but not for update.

Managing locks has disadvantages. It can be complex to program. It requires significant


database management resources, and it can cause performance problems as the
number of users of an application increases. For these reasons, not all database
management systems support pessimistic concurrency. Entity Framework Core provides
no built-in support for it, and this tutorial doesn't show you how to implement it.

Optimistic Concurrency
The alternative to pessimistic concurrency is optimistic concurrency. Optimistic
concurrency means allowing concurrency conflicts to happen, and then reacting
appropriately if they do. For example, Jane visits the Department Edit page and changes
the Budget amount for the English department from $350,000.00 to $0.00.
Before Jane clicks Save, John visits the same page and changes the Start Date field from
9/1/2007 to 9/1/2013.
Jane clicks Save first and sees her change when the browser returns to the Index page.
Then John clicks Save on an Edit page that still shows a budget of $350,000.00. What
happens next is determined by how you handle concurrency conflicts.

Some of the options include the following:

You can keep track of which property a user has modified and update only the
corresponding columns in the database.

In the example scenario, no data would be lost, because different properties were
updated by the two users. The next time someone browses the English
department, they will see both Jane's and John's changes -- a start date of
9/1/2013 and a budget of zero dollars. This method of updating can reduce the
number of conflicts that could result in data loss, but it can't avoid data loss if
competing changes are made to the same property of an entity. Whether the
Entity Framework works this way depends on how you implement your update
code. It's often not practical in a web application, because it can require that you
maintain large amounts of state in order to keep track of all original property
values for an entity as well as new values. Maintaining large amounts of state can
affect application performance because it either requires server resources or must
be included in the web page itself (for example, in hidden fields) or in a cookie.

You can let John's change overwrite Jane's change.

The next time someone browses the English department, they will see 9/1/2013
and the restored $350,000.00 value. This is called a Client Wins or Last in Wins
scenario. (All values from the client take precedence over what's in the data store.)
As noted in the introduction to this section, if you don't do any coding for
concurrency handling, this will happen automatically.

You can prevent John's change from being updated in the database.

Typically, you would display an error message, show him the current state of the
data, and allow him to reapply his changes if he still wants to make them. This is
called a Store Wins scenario. (The data-store values take precedence over the
values submitted by the client.) You'll implement the Store Wins scenario in this
tutorial. This method ensures that no changes are overwritten without a user being
alerted to what's happening.

Detecting concurrency conflicts


You can resolve conflicts by handling DbConcurrencyException exceptions that the Entity
Framework throws. In order to know when to throw these exceptions, the Entity
Framework must be able to detect conflicts. Therefore, you must configure the database
and the data model appropriately. Some options for enabling conflict detection include
the following:

In the database table, include a tracking column that can be used to determine
when a row has been changed. You can then configure the Entity Framework to
include that column in the Where clause of SQL Update or Delete commands.

The data type of the tracking column is typically rowversion . The rowversion value
is a sequential number that's incremented each time the row is updated. In an
Update or Delete command, the Where clause includes the original value of the
tracking column (the original row version) . If the row being updated has been
changed by another user, the value in the rowversion column is different than the
original value, so the Update or Delete statement can't find the row to update
because of the Where clause. When the Entity Framework finds that no rows have
been updated by the Update or Delete command (that is, when the number of
affected rows is zero), it interprets that as a concurrency conflict.

Configure the Entity Framework to include the original values of every column in
the table in the Where clause of Update and Delete commands.

As in the first option, if anything in the row has changed since the row was first
read, the Where clause won't return a row to update, which the Entity Framework
interprets as a concurrency conflict. For database tables that have many columns,
this approach can result in very large Where clauses, and can require that you
maintain large amounts of state. As noted earlier, maintaining large amounts of
state can affect application performance. Therefore this approach is generally not
recommended, and it isn't the method used in this tutorial.

If you do want to implement this approach to concurrency, you have to mark all
non-primary-key properties in the entity you want to track concurrency for by
adding the ConcurrencyCheck attribute to them. That change enables the Entity
Framework to include all columns in the SQL Where clause of Update and Delete
statements.

In the remainder of this tutorial you'll add a rowversion tracking property to the
Department entity, create a controller and views, and test to verify that everything works
correctly.

Add a tracking property


In Models/Department.cs , add a tracking property named RowVersion:
C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { 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; }

public int? InstructorID { get; set; }

[Timestamp]
public byte[] RowVersion { get; set; }

public Instructor Administrator { get; set; }


public ICollection<Course> Courses { get; set; }
}
}

The Timestamp attribute specifies that this column will be included in the Where clause
of Update and Delete commands sent to the database. The attribute is called Timestamp
because previous versions of SQL Server used a SQL timestamp data type before the
SQL rowversion replaced it. The .NET type for rowversion is a byte array.

If you prefer to use the fluent API, you can use the IsConcurrencyToken method (in
Data/SchoolContext.cs ) to specify the tracking property, as shown in the following

example:

C#

modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();
By adding a property you changed the database model, so you need to do another
migration.

Save your changes and build the project, and then enter the following commands in the
command window:

.NET CLI

dotnet ef migrations add RowVersion

.NET CLI

dotnet ef database update

Create Departments controller and views


Scaffold a Departments controller and views as you did earlier for Students, Courses,
and Instructors.

In the DepartmentsController.cs file, change all four occurrences of "FirstMidName" to


"FullName" so that the department administrator drop-down lists will contain the full
name of the instructor rather than just the last name.

C#

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID",


"FullName", department.InstructorID);
Update Index view
The scaffolding engine created a RowVersion column in the Index view, but that field
shouldn't be displayed.

Replace the code in Views/Departments/Index.cshtml with the following code.

CSHTML

@model IEnumerable<ContosoUniversity.Models.Department>

@{
ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Administrator)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem =>
item.Administrator.FullName)
</td>
<td>
<a asp-action="Edit" asp-route-
id="@item.DepartmentID">Edit</a> |
<a asp-action="Details" asp-route-
id="@item.DepartmentID">Details</a> |
<a asp-action="Delete" asp-route-
id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>

This changes the heading to "Departments", deletes the RowVersion column, and shows
full name instead of first name for the administrator.

Update Edit methods


In both the HttpGet Edit method and the Details method, add AsNoTracking . In the
HttpGet Edit method, add eager loading for the Administrator.

C#

var department = await _context.Departments


.Include(i => i.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);

Replace the existing code for the HttpPost Edit method with the following code:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
if (id == null)
{
return NotFound();
}

var departmentToUpdate = await _context.Departments.Include(i =>


i.Administrator).FirstOrDefaultAsync(m => m.DepartmentID == id);

if (departmentToUpdate == null)
{
Department deletedDepartment = new Department();
await TryUpdateModelAsync(deletedDepartment);
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another
user.");
ViewData["InstructorID"] = new SelectList(_context.Instructors,
"ID", "FullName", deletedDepartment.InstructorID);
return View(deletedDepartment);
}

_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue
= rowVersion;

if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by
another user.");
}
else
{
var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value:
{databaseValues.Name}");
}
if (databaseValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Budget", $"Current value:
{databaseValues.Budget:c}");
}
if (databaseValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("StartDate", $"Current value:
{databaseValues.StartDate:d}");
}
if (databaseValues.InstructorID !=
clientValues.InstructorID)
{
Instructor databaseInstructor = await
_context.Instructors.FirstOrDefaultAsync(i => i.ID ==
databaseValues.InstructorID);
ModelState.AddModelError("InstructorID", $"Current
value: {databaseInstructor?.FullName}");
}

ModelState.AddModelError(string.Empty, "The record you


attempted to edit "
+ "was modified by another user after you got the
original value. The "
+ "edit operation was canceled and the current
values in the database "
+ "have been displayed. If you still want to edit
this record, click "
+ "the Save button again. Otherwise click the Back
to List hyperlink.");
departmentToUpdate.RowVersion =
(byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
}
}
}
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID",
"FullName", departmentToUpdate.InstructorID);
return View(departmentToUpdate);
}

The code begins by trying to read the department to be updated. If the


FirstOrDefaultAsync method returns null, the department was deleted by another user.

In that case the code uses the posted form values to create a Department entity so that
the Edit page can be redisplayed with an error message. As an alternative, you wouldn't
have to re-create the Department entity if you display only an error message without
redisplaying the department fields.

The view stores the original RowVersion value in a hidden field, and this method receives
that value in the rowVersion parameter. Before you call SaveChanges , you have to put
that original RowVersion property value in the OriginalValues collection for the entity.

C#

_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue =
rowVersion;

Then when the Entity Framework creates a SQL UPDATE command, that command will
include a WHERE clause that looks for a row that has the original RowVersion value. If no
rows are affected by the UPDATE command (no rows have the original RowVersion
value), the Entity Framework throws a DbUpdateConcurrencyException exception.

The code in the catch block for that exception gets the affected Department entity that
has the updated values from the Entries property on the exception object.

C#

var exceptionEntry = ex.Entries.Single();

The Entries collection will have just one EntityEntry object. You can use that object to
get the new values entered by the user and the current database values.

C#

var clientValues = (Department)exceptionEntry.Entity;


var databaseEntry = exceptionEntry.GetDatabaseValues();

The code adds a custom error message for each column that has database values
different from what the user entered on the Edit page (only one field is shown here for
brevity).

C#

var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value:
{databaseValues.Name}");

Finally, the code sets the RowVersion value of the departmentToUpdate to the new value
retrieved from the database. This new RowVersion value will be stored in the hidden field
when the Edit page is redisplayed, and the next time the user clicks Save, only
concurrency errors that happen since the redisplay of the Edit page will be caught.

C#

departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");

The ModelState.Remove statement is required because ModelState has the old


RowVersion value. In the view, the ModelState value for a field takes precedence over

the model property values when both are present.


Update Edit view
In Views/Departments/Edit.cshtml , make the following changes:

Add a hidden field to save the RowVersion property value, immediately following
the hidden field for the DepartmentID property.

Add a "Select Administrator" option to the drop-down list.

CSHTML

@model ContosoUniversity.Models.Department

@{
ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<h4>Department</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="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Budget" class="control-label"></label>
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label"></label>
<select asp-for="InstructorID" class="form-control" asp-
items="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
<span asp-validation-for="InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Test concurrency conflicts


Run the app and go to the Departments Index page. Right-click the Edit hyperlink for
the English department and select Open in new tab, then click the Edit hyperlink for the
English department. The two browser tabs now display the same information.

Change a field in the first browser tab and click Save.


The browser shows the Index page with the changed value.

Change a field in the second browser tab.


Click Save. You see an error message:
Click Save again. The value you entered in the second browser tab is saved. You see the
saved values when the Index page appears.

Update the Delete page


For the Delete page, the Entity Framework detects concurrency conflicts caused by
someone else editing the department in a similar manner. When the HttpGet Delete
method displays the confirmation view, the view includes the original RowVersion value
in a hidden field. That value is then available to the HttpPost Delete method that's
called when the user confirms the deletion. When the Entity Framework creates the SQL
DELETE command, it includes a WHERE clause with the original RowVersion value. If the
command results in zero rows affected (meaning the row was changed after the Delete
confirmation page was displayed), a concurrency exception is thrown, and the HttpGet
Delete method is called with an error flag set to true in order to redisplay the
confirmation page with an error message. It's also possible that zero rows were affected
because the row was deleted by another user, so in that case no error message is
displayed.

Update the Delete methods in the Departments controller


In DepartmentsController.cs , replace the HttpGet Delete method with the following
code:

C#

public async Task<IActionResult> Delete(int? id, bool? concurrencyError)


{
if (id == null)
{
return NotFound();
}

var department = await _context.Departments


.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (department == null)
{
if (concurrencyError.GetValueOrDefault())
{
return RedirectToAction(nameof(Index));
}
return NotFound();
}

if (concurrencyError.GetValueOrDefault())
{
ViewData["ConcurrencyErrorMessage"] = "The record you attempted to
delete "
+ "was modified by another user after you got the original
values. "
+ "The delete operation was canceled and the current values in
the "
+ "database have been displayed. If you still want to delete
this "
+ "record, click the Delete button again. Otherwise "
+ "click the Back to List hyperlink.";
}
return View(department);
}

The method accepts an optional parameter that indicates whether the page is being
redisplayed after a concurrency error. If this flag is true and the department specified no
longer exists, it was deleted by another user. In that case, the code redirects to the Index
page. If this flag is true and the department does exist, it was changed by another user.
In that case, the code sends an error message to the view using ViewData .

Replace the code in the HttpPost Delete method (named DeleteConfirmed ) with the
following code:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
try
{
if (await _context.Departments.AnyAsync(m => m.DepartmentID ==
department.DepartmentID))
{
_context.Departments.Remove(department);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { concurrencyError =
true, id = department.DepartmentID });