Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
958 views

Aspnet Core Aspnetcore 7.0

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
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
958 views

Aspnet Core Aspnetcore 7.0

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
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("bob42@jabbr.net",
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 bob42@jabbr.net 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 });
}
}

In the scaffolded code that you just replaced, this method accepted only a record ID:

C#

public async Task<IActionResult> DeleteConfirmed(int id)

You've changed this parameter to a Department entity instance created by the model
binder. This gives EF access to the RowVers`ion property value in addition to the record
key.

C#
public async Task<IActionResult> Delete(Department department)

You have also changed the action method name from DeleteConfirmed to Delete . The
scaffolded code used the name DeleteConfirmed to give the HttpPost method a unique
signature. (The CLR requires overloaded methods to have different method parameters.)
Now that the signatures are unique, you can stick with the MVC convention and use the
same name for the HttpPost and HttpGet delete methods.

If the department is already deleted, the AnyAsync method returns false and the
application just goes back to the Index method.

If a concurrency error is caught, the code redisplays the Delete confirmation page and
provides a flag that indicates it should display a concurrency error message.

Update the Delete view


In Views/Departments/Delete.cshtml , replace the scaffolded code with the following
code that adds an error message field and hidden fields for the DepartmentID and
RowVersion properties. The changes are highlighted.

CSHTML

@model ContosoUniversity.Models.Department

@{
ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<p class="text-danger">@ViewData["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.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>

<form asp-action="Delete">
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<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>

This makes the following changes:

Adds an error message between the h2 and h3 headings.

Replaces FirstMidName with FullName in the Administrator field.

Removes the RowVersion field.

Adds a hidden field for the RowVersion property.

Run the app and go to the Departments Index page. Right-click the Delete hyperlink for
the English department and select Open in new tab, then in the first tab click the Edit
hyperlink for the English department.

In the first window, change one of the values, and click Save:
In the second tab, click Delete. You see the concurrency error message, and the
Department values are refreshed with what's currently in the database.
If you click Delete again, you're redirected to the Index page, which shows that the
department has been deleted.

Update Details and Create views


You can optionally clean up scaffolded code in the Details and Create views.

Replace the code in Views/Departments/Details.cshtml to delete the RowVersion


column and show the full name of the Administrator.

CSHTML

@model ContosoUniversity.Models.Department

@{
ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
<h4>Department</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>

Replace the code in Views/Departments/Create.cshtml to add a Select option to the


drop-down list.

CSHTML

@model ContosoUniversity.Models.Department

@{
ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Department</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="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>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default"
/>
</div>
</form>
</div>
</div>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Get the code


Download or view the completed application.
Additional resources
For more information about how to handle concurrency in EF Core, see Concurrency
conflicts.

Next steps
In this tutorial, you:

" Learned about concurrency conflicts


" Added a tracking property
" Created Departments controller and views
" Updated Index view
" Updated Edit methods
" Updated Edit view
" Tested concurrency conflicts
" Updated the Delete page
" Updated Details and Create views

Advance to the next tutorial to learn how to implement table-per-hierarchy inheritance


for the Instructor and Student entities.

Next: Implement table-per-hierarchy inheritance


Tutorial: Implement inheritance -
ASP.NET MVC with EF Core
Article • 04/11/2023

In the previous tutorial, you handled concurrency exceptions. This tutorial will show you
how to implement inheritance in the data model.

In object-oriented programming, you can use inheritance to facilitate code reuse. In this
tutorial, you'll change the Instructor and Student classes so that they derive from a
Person base class which contains properties such as LastName that are common to both

instructors and students. You won't add or change any web pages, but you'll change
some of the code and those changes will be automatically reflected in the database.

In this tutorial, you:

" Map inheritance to database


" Create the Person class
" Update Instructor and Student
" Add Person to the model
" Create and update migrations
" Test the implementation

Prerequisites
Handle Concurrency

Map inheritance to database


The Instructor and Student classes in the School data model have several properties
that are identical:
Suppose you want to eliminate the redundant code for the properties that are shared by
the Instructor and Student entities. Or you want to write a service that can format
names without caring whether the name came from an instructor or a student. You
could create a Person base class that contains only those shared properties, then make
the Instructor and Student classes inherit from that base class, as shown in the
following illustration:

There are several ways this inheritance structure could be represented in the database.
You could have a Person table that includes information about both students and
instructors in a single table. Some of the columns could apply only to instructors
(HireDate), some only to students (EnrollmentDate), some to both (LastName,
FirstName). Typically, you'd have a discriminator column to indicate which type each row
represents. For example, the discriminator column might have "Instructor" for instructors
and "Student" for students.

This pattern of generating an entity inheritance structure from a single database table is
called table-per-hierarchy (TPH) inheritance.

An alternative is to make the database look more like the inheritance structure. For
example, you could have only the name fields in the Person table and have separate
Instructor and Student tables with the date fields.

2 Warning

Table-Per-Type (TPT) is not supported by EF Core 3.x, however it is has been


implemented in EF Core 5.0.

This pattern of making a database table for each entity class is called table-per-type
(TPT) inheritance.

Yet another option is to map all non-abstract types to individual tables. All properties of
a class, including inherited properties, map to columns of the corresponding table. This
pattern is called Table-per-Concrete Class (TPC) inheritance. If you implemented TPC
inheritance for the Person , Student , and Instructor classes as shown earlier, the
Student and Instructor tables would look no different after implementing inheritance
than they did before.

TPC and TPH inheritance patterns generally deliver better performance than TPT
inheritance patterns, because TPT patterns can result in complex join queries.

This tutorial demonstrates how to implement TPH inheritance. TPH is the only
inheritance pattern that the Entity Framework Core supports. What you'll do is create a
Person class, change the Instructor and Student classes to derive from Person , add

the new class to the DbContext , and create a migration.

 Tip

Consider saving a copy of the project before making the following changes. Then if
you run into problems and need to start over, it will be easier to start from the
saved project instead of reversing steps done for this tutorial or going back to the
beginning of the whole series.

Create the Person class


In the Models folder, create Person.cs and replace the template code with the following
code:

C#

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public abstract class Person
{
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; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
}
}

Update Instructor and Student


In Instructor.cs , derive the Instructor class from the Person class and remove the key
and name fields. The code will look like the following example:

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Instructor : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }

public ICollection<CourseAssignment> CourseAssignments { get; set; }


public OfficeAssignment OfficeAssignment { get; set; }
}
}

Make the same changes in Student.cs .

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
public class Student : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }

public ICollection<Enrollment> Enrollments { get; set; }


}
}

Add Person to the model


Add the Person entity type to SchoolContext.cs . The new lines are highlighted.

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; }
public DbSet<Person> People { 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<Person>().ToTable("Person");

modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}

This is all that the Entity Framework needs in order to configure table-per-hierarchy
inheritance. As you'll see, when the database is updated, it will have a Person table in
place of the Student and Instructor tables.

Create and update migrations


Save your changes and build the project. Then open the command window in the
project folder and enter the following command:

.NET CLI

dotnet ef migrations add Inheritance

Don't run the database update command yet. That command will result in lost data
because it will drop the Instructor table and rename the Student table to Person. You
need to provide custom code to preserve existing data.

Open Migrations/<timestamp>_Inheritance.cs and replace the Up method with the


following code:

C#

protected override void Up(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropForeignKey(
name: "FK_Enrollment_Student_StudentID",
table: "Enrollment");

migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table:


"Enrollment");

migrationBuilder.RenameTable(name: "Instructor", newName: "Person");


migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table:
"Person", nullable: true);
migrationBuilder.AddColumn<string>(name: "Discriminator", table:
"Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table:
"Person", nullable: true);
migrationBuilder.AddColumn<int>(name: "OldId", table: "Person",
nullable: true);

// Copy existing Student data into new Person table.


migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName,
HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName,
null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId
FROM dbo.Student");
// Fix up existing relationships to match new PK's.
migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID
FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator =
'Student')");

// Remove temporary key


migrationBuilder.DropColumn(name: "OldID", table: "Person");

migrationBuilder.DropTable(
name: "Student");

migrationBuilder.CreateIndex(
name: "IX_Enrollment_StudentID",
table: "Enrollment",
column: "StudentID");

migrationBuilder.AddForeignKey(
name: "FK_Enrollment_Person_StudentID",
table: "Enrollment",
column: "StudentID",
principalTable: "Person",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
}

This code takes care of the following database update tasks:

Removes foreign key constraints and indexes that point to the Student table.

Renames the Instructor table as Person and makes changes needed for it to store
Student data:

Adds nullable EnrollmentDate for students.

Adds Discriminator column to indicate whether a row is for a student or an


instructor.

Makes HireDate nullable since student rows won't have hire dates.

Adds a temporary field that will be used to update foreign keys that point to
students. When you copy students into the Person table they will get new primary
key values.
Copies data from the Student table into the Person table. This causes students to
get assigned new primary key values.

Fixes foreign key values that point to students.

Re-creates foreign key constraints and indexes, now pointing them to the Person
table.

(If you had used GUID instead of integer as the primary key type, the student primary
key values wouldn't have to change, and several of these steps could have been
omitted.)

Run the database update command:

.NET CLI

dotnet ef database update

(In a production system you would make corresponding changes to the Down method in
case you ever had to use that to go back to the previous database version. For this
tutorial you won't be using the Down method.)

7 Note

It's possible to get other errors when making schema changes in a database that
has existing data. If you get migration errors that you can't resolve, you can either
change the database name in the connection string or delete the database. With a
new database, there's no data to migrate, and the update-database command is
more likely to complete without errors. To delete the database, use SSOX or run the
database drop CLI command.

Test the implementation


Run the app and try various pages. Everything works the same as it did before.

In SQL Server Object Explorer, expand Data Connections/SchoolContext and then


Tables, and you see that the Student and Instructor tables have been replaced by a
Person table. Open the Person table designer and you see that it has all of the columns
that used to be in the Student and Instructor tables.
Right-click the Person table, and then click Show Table Data to see the discriminator
column.

Get the code


Download or view the completed application.
Additional resources
For more information about inheritance in Entity Framework Core, see Inheritance.

Next steps
In this tutorial, you:

" Mapped inheritance to database


" Created the Person class
" Updated Instructor and Student
" Added Person to the model
" Created and update migrations
" Tested the implementation

Advance to the next tutorial to learn how to handle a variety of relatively advanced
Entity Framework scenarios.

Next: Advanced topics


Tutorial: Learn about advanced
scenarios - ASP.NET MVC with EF Core
Article • 04/11/2023

In the previous tutorial, you implemented table-per-hierarchy inheritance. This tutorial


introduces several topics that are useful to be aware of when you go beyond the basics
of developing ASP.NET Core web applications that use Entity Framework Core.

In this tutorial, you:

" Perform raw SQL queries


" Call a query to return entities
" Call a query to return other types
" Call an update query
" Examine SQL queries
" Create an abstraction layer
" Learn about Automatic change detection
" Learn about EF Core source code and development plans
" Learn how to use dynamic LINQ to simplify code

Prerequisites
Implement Inheritance

Perform raw SQL queries


One of the advantages of using the Entity Framework is that it avoids tying your code
too closely to a particular method of storing data. It does this by generating SQL queries
and commands for you, which also frees you from having to write them yourself. But
there are exceptional scenarios when you need to run specific SQL queries that you have
manually created. For these scenarios, the Entity Framework Code First API includes
methods that enable you to pass SQL commands directly to the database. You have the
following options in EF Core 1.0:

Use the DbSet.FromSql method for queries that return entity types. The returned
objects must be of the type expected by the DbSet object, and they're
automatically tracked by the database context unless you turn tracking off.

Use the Database.ExecuteSqlCommand for non-query commands.


If you need to run a query that returns types that aren't entities, you can use ADO.NET
with the database connection provided by EF. The returned data isn't tracked by the
database context, even if you use this method to retrieve entity types.

As is always true when you execute SQL commands in a web application, you must take
precautions to protect your site against SQL injection attacks. One way to do that is to
use parameterized queries to make sure that strings submitted by a web page can't be
interpreted as SQL commands. In this tutorial you'll use parameterized queries when
integrating user input into a query.

Call a query to return entities


The DbSet<TEntity> class provides a method that you can use to execute a query that
returns an entity of type TEntity . To see how this works you'll change the code in the
Details method of the Department controller.

In DepartmentsController.cs , in the Details method, replace the code that retrieves a


department with a FromSql method call, as shown in the following highlighted code:

C#

public async Task<IActionResult> Details(int? id)


{
if (id == null)
{
return NotFound();
}

string query = "SELECT * FROM Department WHERE DepartmentID = {0}";


var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync();

if (department == null)
{
return NotFound();
}

return View(department);
}

To verify that the new code works correctly, select the Departments tab and then
Details for one of the departments.
Call a query to return other types
Earlier you created a student statistics grid for the About page that showed the number
of students for each enrollment date. You got the data from the Students entity set
( _context.Students ) and used LINQ to project the results into a list of
EnrollmentDateGroup view model objects. Suppose you want to write the SQL itself
rather than using LINQ. To do that you need to run a SQL query that returns something
other than entity objects. In EF Core 1.0, one way to do that is to write ADO.NET code
and get the database connection from EF.

In HomeController.cs , replace the About method with the following code:

C#

public async Task<ActionResult> About()


{
List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount
"
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();

if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate =
reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}

Add a using statement:

C#

using System.Data.Common;

Run the app and go to the About page. It displays the same data it did before.
Call an update query
Suppose Contoso University administrators want to perform global changes in the
database, such as changing the number of credits for every course. If the university has
a large number of courses, it would be inefficient to retrieve them all as entities and
change them individually. In this section you'll implement a web page that enables the
user to specify a factor by which to change the number of credits for all courses, and
you'll make the change by executing a SQL UPDATE statement. The web page will look
like the following illustration:

In CoursesController.cs , add UpdateCourseCredits methods for HttpGet and HttpPost:

C#

public IActionResult UpdateCourseCredits()


{
return View();
}
C#

[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
if (multiplier != null)
{
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
}
return View();
}

When the controller processes an HttpGet request, nothing is returned in


ViewData["RowsAffected"] , and the view displays an empty text box and a submit
button, as shown in the preceding illustration.

When the Update button is clicked, the HttpPost method is called, and multiplier has
the value entered in the text box. The code then executes the SQL that updates courses
and returns the number of affected rows to the view in ViewData . When the view gets a
RowsAffected value, it displays the number of rows updated.

In Solution Explorer, right-click the Views/Courses folder, and then click Add > New
Item.

In the Add New Item dialog, click ASP.NET Core under Installed in the left pane, click
Razor View, and name the new view UpdateCourseCredits.cshtml .

In Views/Courses/UpdateCourseCredits.cshtml , replace the template code with the


following code:

CSHTML

@{
ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</h2>

@if (ViewData["RowsAffected"] == null)


{
<form asp-action="UpdateCourseCredits">
<div class="form-actions no-color">
<p>
Enter a number to multiply every course's credits by:
@Html.TextBox("multiplier")
</p>
<p>
<input type="submit" value="Update" class="btn btn-default"
/>
</p>
</div>
</form>
}
@if (ViewData["RowsAffected"] != null)
{
<p>
Number of rows updated: @ViewData["RowsAffected"]
</p>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>

Run the UpdateCourseCredits method by selecting the Courses tab, then adding
"/UpdateCourseCredits" to the end of the URL in the browser's address bar (for example:
http://localhost:5813/Courses/UpdateCourseCredits ). Enter a number in the text box:

Click Update. You see the number of rows affected:


Click Back to List to see the list of courses with the revised number of credits.

Note that production code would ensure that updates always result in valid data. The
simplified code shown here could multiply the number of credits enough to result in
numbers greater than 5. (The Credits property has a [Range(0, 5)] attribute.) The
update query would work but the invalid data could cause unexpected results in other
parts of the system that assume the number of credits is 5 or less.

For more information about raw SQL queries, see Raw SQL Queries.

Examine SQL queries


Sometimes it's helpful to be able to see the actual SQL queries that are sent to the
database. Built-in logging functionality for ASP.NET Core is automatically used by EF
Core to write logs that contain the SQL for queries and updates. In this section you'll see
some examples of SQL logging.

Open StudentsController.cs and in the Details method set a breakpoint on the if


(student == null) statement.

Run the app in debug mode, and go to the Details page for a student.

Go to the Output window showing debug output, and you see the query:

Microsoft.EntityFrameworkCore.Database.Command:Information: Executed
DbCommand (56ms) [Parameters=[@__id_0='?'], CommandType='Text',
CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].
[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed
DbCommand (122ms) [Parameters=[@__id_0='?'], CommandType='Text',
CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID],
[s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID],
[e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] =
[e.Course].[CourseID]
INNER JOIN (
SELECT TOP(1) [s0].[ID]
FROM [Person] AS [s0]
WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]

You'll notice something here that might surprise you: the SQL selects up to 2 rows
( TOP(2) ) from the Person table. The SingleOrDefaultAsync method doesn't resolve to 1
row on the server. Here's why:

If the query would return multiple rows, the method returns null.
To determine whether the query would return multiple rows, EF has to check if it
returns at least 2.

Note that you don't have to use debug mode and stop at a breakpoint to get logging
output in the Output window. It's just a convenient way to stop the logging at the point
you want to look at the output. If you don't do that, logging continues and you have to
scroll back to find the parts you're interested in.

Create an abstraction layer


Many developers write code to implement the repository and unit of work patterns as a
wrapper around code that works with the Entity Framework. These patterns are intended
to create an abstraction layer between the data access layer and the business logic layer
of an application. Implementing these patterns can help insulate your application from
changes in the data store and can facilitate automated unit testing or test-driven
development (TDD). However, writing additional code to implement these patterns isn't
always the best choice for applications that use EF, for several reasons:

The EF context class itself insulates your code from data-store-specific code.

The EF context class can act as a unit-of-work class for database updates that you
do using EF.

EF includes features for implementing TDD without writing repository code.


For information about how to implement the repository and unit of work patterns, see
the Entity Framework 5 version of this tutorial series.

Entity Framework Core implements an in-memory database provider that can be used
for testing. For more information, see Test with InMemory.

Automatic change detection


The Entity Framework determines how an entity has changed (and therefore which
updates need to be sent to the database) by comparing the current values of an entity
with the original values. The original values are stored when the entity is queried or
attached. Some of the methods that cause automatic change detection are the
following:

DbContext.SaveChanges

DbContext.Entry

ChangeTracker.Entries

If you're tracking a large number of entities and you call one of these methods many
times in a loop, you might get significant performance improvements by temporarily
turning off automatic change detection using the
ChangeTracker.AutoDetectChangesEnabled property. For example:

C#

_context.ChangeTracker.AutoDetectChangesEnabled = false;

EF Core source code and development plans


The Entity Framework Core source is at https://github.com/dotnet/efcore . The EF Core
repository contains nightly builds, issue tracking, feature specs, design meeting notes,
and the roadmap for future development . You can file or find bugs, and contribute.

Although the source code is open, Entity Framework Core is fully supported as a
Microsoft product. The Microsoft Entity Framework team keeps control over which
contributions are accepted and tests all code changes to ensure the quality of each
release.

Reverse engineer from existing database


To reverse engineer a data model including entity classes from an existing database, use
the scaffold-dbcontext command. See the getting-started tutorial.

Use dynamic LINQ to simplify code


The third tutorial in this series shows how to write LINQ code by hard-coding column
names in a switch statement. With two columns to choose from, this works fine, but if
you have many columns the code could get verbose. To solve that problem, you can use
the EF.Property method to specify the name of the property as a string. To try out this
approach, replace the Index method in the StudentsController 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) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" :
"EnrollmentDate";

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));
}

if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}
bool descending = false;
if (sortOrder.EndsWith("_desc"))
{
sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
descending = true;
}

if (descending)
{
students = students.OrderByDescending(e => EF.Property<object>(e,
sortOrder));
}
else
{
students = students.OrderBy(e => EF.Property<object>(e,
sortOrder));
}

int pageSize = 3;
return View(await
PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
pageNumber ?? 1, pageSize));
}

Acknowledgments
Tom Dykstra and Rick Anderson (twitter @RickAndMSFT) wrote this tutorial. Rowan
Miller, Diego Vega, and other members of the Entity Framework team assisted with code
reviews and helped debug issues that arose while we were writing code for the tutorials.
John Parente and Paul Goldman worked on updating the tutorial for ASP.NET Core 2.2.

Troubleshoot common errors

ContosoUniversity.dll used by another process


Error message:

Cannot open '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The


process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll'
because it is being used by another process.

Solution:
Stop the site in IIS Express. Go to the Windows System Tray, find IIS Express and right-
click its icon, select the Contoso University site, and then click Stop Site.

Migration scaffolded with no code in Up and Down


methods
Possible cause:

The EF CLI commands don't automatically close and save code files. If you have unsaved
changes when you run the migrations add command, EF won't find your changes.

Solution:

Run the migrations remove command, save your code changes and rerun the
migrations add command.

Errors while running database update


It's possible to get other errors when making schema changes in a database that has
existing data. If you get migration errors you can't resolve, you can either change the
database name in the connection string or delete the database. With a new database,
there's no data to migrate, and the update-database command is much more likely to
complete without errors.

The simplest approach is to rename the database in appsettings.json . The next time
you run database update , a new database will be created.

To delete a database in SSOX, right-click the database, click Delete, and then in the
Delete Database dialog box select Close existing connections and click OK.

To delete a database by using the CLI, run the database drop CLI command:

.NET CLI

dotnet ef database drop

Error locating SQL Server instance


Error Message:

A network-related or instance-specific error occurred while establishing a


connection to SQL Server. The server was not found or was not accessible. Verify
that the instance name is correct and that SQL Server is configured to allow remote
connections. (provider: SQL Network Interfaces, error: 26 - Error Locating
Server/Instance Specified)

Solution:

Check the connection string. If you have manually deleted the database file, change the
name of the database in the construction string to start over with a new database.

Get the code


Download or view the completed application.

Additional resources
For more information about EF Core, see the Entity Framework Core documentation. A
book is also available: Entity Framework Core in Action .

For information on how to deploy a web app, see Host and deploy ASP.NET Core.

For information about other topics related to ASP.NET Core MVC, such as authentication
and authorization, see Overview of ASP.NET Core.

Next steps
In this tutorial, you:

" Performed raw SQL queries


" Called a query to return entities
" Called a query to return other types
" Called an update query
" Examined SQL queries
" Created an abstraction layer
" Learned about Automatic change detection
" Learned about EF Core source code and development plans
" Learned how to use dynamic LINQ to simplify code

This completes this series of tutorials on using the Entity Framework Core in an ASP.NET
Core MVC application. This series worked with a new database; an alternative is to
reverse engineer a model from an existing database.
Tutorial: EF Core with MVC, existing database
ASP.NET Core fundamentals overview
Article • 04/11/2023

This article provides an overview of the fundamentals for building ASP.NET Core apps,
including dependency injection (DI), configuration, middleware, and more.

Program.cs
ASP.NET Core apps created with the web templates contain the application startup code
in the Program.cs file. The Program.cs file is where:

Services required by the app are configured.


The app's request handling pipeline is defined as a series of middleware
components.

The following app startup code supports:

Razor Pages
MVC controllers with views
Web API with controllers
Minimal web APIs

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

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

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

Dependency injection (services)


ASP.NET Core includes dependency injection (DI) that makes configured services
available throughout an app. Services are added to the DI container with
WebApplicationBuilder.Services, builder.Services in the preceding code. When the
WebApplicationBuilder is instantiated, many framework-provided services are added.
builder is a WebApplicationBuilder in the following code:

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

In the preceding highlighted code, builder has configuration, logging, and many other
services added to the DI container.

The following code adds Razor Pages, MVC controllers with views, and a custom
DbContext to the DI container:

C#

using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<RazorPagesMovieContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("RPMovieConte
xt")));

var app = builder.Build();


Services are typically resolved from DI using constructor injection. The DI framework
provides an instance of this service at runtime.

The following code uses constructor injection to resolve the database context and
logger from DI:

C#

public class IndexModel : PageModel


{
private readonly RazorPagesMovieContext _context;
private readonly ILogger<IndexModel> _logger;

public IndexModel(RazorPagesMovieContext context, ILogger<IndexModel>


logger)
{
_context = context;
_logger = logger;
}

public IList<Movie> Movie { get;set; }

public async Task OnGetAsync()


{
_logger.LogInformation("IndexModel OnGetAsync.");
Movie = await _context.Movie.ToListAsync();
}
}

Middleware
The request handling pipeline is composed as a series of middleware components. Each
component performs operations on an HttpContext and either invokes the next
middleware in the pipeline or terminates the request.

By convention, a middleware component is added to the pipeline by invoking a


Use{Feature} extension method. Middleware added to the app is highlighted in the

following code:

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();


// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

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

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

For more information, see ASP.NET Core Middleware.

Host
On startup, an ASP.NET Core app builds a host. The host encapsulates all of the app's
resources, such as:

An HTTP server implementation


Middleware components
Logging
Dependency injection (DI) services
Configuration

There are three different hosts capable of running an ASP.NET Core app:

ASP.NET Core WebApplication, also known as the Minimal Host


.NET Generic Host combined with ASP.NET Core's ConfigureWebHostDefaults
ASP.NET Core WebHost

The ASP.NET Core WebApplication and WebApplicationBuilder types are recommended


and used in all the ASP.NET Core templates. WebApplication behaves similarly to the
.NET Generic Host and exposes many of the same interfaces but requires less callbacks
to configure. The ASP.NET Core WebHost is available only for backward compatibility.

The following example instantiates a WebApplication :

C#
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

The WebApplicationBuilder.Build method configures a host with a set of default options,


such as:

Use Kestrel as the web server and enable IIS integration.


Load configuration from appsettings.json , environment variables, command line
arguments, and other configuration sources.
Send logging output to the console and debug providers.

Non-web scenarios
The Generic Host allows other types of apps to use cross-cutting framework extensions,
such as logging, dependency injection (DI), configuration, and app lifetime
management. For more information, see .NET Generic Host in ASP.NET Core and
Background tasks with hosted services in ASP.NET Core.

Servers
An ASP.NET Core app uses an HTTP server implementation to listen for HTTP requests.
The server surfaces requests to the app as a set of request features composed into an
HttpContext .

Windows

ASP.NET Core provides the following server implementations:

Kestrel is a cross-platform web server. Kestrel is often run in a reverse proxy


configuration using IIS . In ASP.NET Core 2.0 or later, Kestrel can be run as a
public-facing edge server exposed directly to the Internet.
IIS HTTP Server is a server for Windows that uses IIS. With this server, the
ASP.NET Core app and IIS run in the same process.
HTTP.sys is a server for Windows that isn't used with IIS.

For more information, see Web server implementations in ASP.NET Core.


Configuration
ASP.NET Core provides a configuration framework that gets settings as name-value
pairs from an ordered set of configuration providers. Built-in configuration providers are
available for a variety of sources, such as .json files, .xml files, environment variables,
and command-line arguments. Write custom configuration providers to support other
sources.

By default, ASP.NET Core apps are configured to read from appsettings.json ,


environment variables, the command line, and more. When the app's configuration is
loaded, values from environment variables override values from appsettings.json .

For managing confidential configuration data such as passwords, .NET Core provides the
Secret Manager. For production secrets, we recommend Azure Key Vault.

For more information, see Configuration in ASP.NET Core.

Environments
Execution environments, such as Development , Staging , and Production , are available in
ASP.NET Core. Specify the environment an app is running in by setting the
ASPNETCORE_ENVIRONMENT environment variable. ASP.NET Core reads that environment
variable at app startup and stores the value in an IWebHostEnvironment implementation.
This implementation is available anywhere in an app via dependency injection (DI).

The following example configures the exception handler and HTTP Strict Transport
Security Protocol (HSTS) middleware when not running in the Development environment:

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

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

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

For more information, see Use multiple environments in ASP.NET Core.

Logging
ASP.NET Core supports a logging API that works with a variety of built-in and third-
party logging providers. Available providers include:

Console
Debug
Event Tracing on Windows
Windows Event Log
TraceSource
Azure App Service
Azure Application Insights

To create logs, resolve an ILogger<TCategoryName> service from dependency injection


(DI) and call logging methods such as LogInformation. For example:

C#

public class IndexModel : PageModel


{
private readonly RazorPagesMovieContext _context;
private readonly ILogger<IndexModel> _logger;

public IndexModel(RazorPagesMovieContext context, ILogger<IndexModel>


logger)
{
_context = context;
_logger = logger;
}

public IList<Movie> Movie { get;set; }

public async Task OnGetAsync()


{
_logger.LogInformation("IndexModel OnGetAsync.");
Movie = await _context.Movie.ToListAsync();
}
}

For more information, see Logging in .NET Core and ASP.NET Core.

Routing
A route is a URL pattern that is mapped to a handler. The handler is typically a Razor
page, an action method in an MVC controller, or a middleware. ASP.NET Core routing
gives you control over the URLs used by your app.

The following code, generated by the ASP.NET Core web application template, calls
UseRouting:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

For more information, see Routing in ASP.NET Core.

Error handling
ASP.NET Core has built-in features for handling errors, such as:
A developer exception page
Custom error pages
Static status code pages
Startup exception handling

For more information, see Handle errors in ASP.NET Core.

Make HTTP requests


An implementation of IHttpClientFactory is available for creating HttpClient instances.
The factory:

Provides a central location for naming and configuring logical HttpClient


instances. For example, register and configure a github client for accessing GitHub.
Register and configure a default client for other purposes.
Supports registration and chaining of multiple delegating handlers to build an
outgoing request middleware pipeline. This pattern is similar to ASP.NET Core's
inbound middleware pipeline. The pattern provides a mechanism to manage cross-
cutting concerns for HTTP requests, including caching, error handling, serialization,
and logging.
Integrates with Polly, a popular third-party library for transient fault handling.
Manages the pooling and lifetime of underlying HttpClientHandler instances to
avoid common DNS problems that occur when managing HttpClient lifetimes
manually.
Adds a configurable logging experience via ILogger for all requests sent through
clients created by the factory.

For more information, see Make HTTP requests using IHttpClientFactory in ASP.NET
Core.

Content root
The content root is the base path for:

The executable hosting the app (.exe).


Compiled assemblies that make up the app (.dll).
Content files used by the app, such as:
Razor files ( .cshtml , .razor )
Configuration files ( .json , .xml )
Data files ( .db )
The Web root, typically the wwwroot folder.
During development, the content root defaults to the project's root directory. This
directory is also the base path for both the app's content files and the Web root. Specify
a different content root by setting its path when building the host. For more
information, see Content root.

Web root
The web root is the base path for public, static resource files, such as:

Stylesheets ( .css )
JavaScript ( .js )
Images ( .png , .jpg )

By default, static files are served only from the web root directory and its sub-
directories. The web root path defaults to {content root}/wwwroot. Specify a different
web root by setting its path when building the host. For more information, see Web
root.

Prevent publishing files in wwwroot with the <Content> project item in the project file.
The following example prevents publishing content in wwwroot/local and its sub-
directories:

XML

<ItemGroup>
<Content Update="wwwroot\local\**\*.*" CopyToPublishDirectory="Never" />
</ItemGroup>

In Razor .cshtml files, ~/ points to the web root. A path beginning with ~/ is referred
to as a virtual path.

For more information, see Static files in ASP.NET Core.

Additional resources
WebApplicationBuilder source code
App startup in ASP.NET Core
Article • 05/09/2023

By Rick Anderson

ASP.NET Core apps created with the web templates contain the application startup code
in the Program.cs file.

The following app startup code supports:

Razor Pages
MVC controllers with views
Web API with controllers
Minimal APIs

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.


if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

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

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

Apps using EventSource can measure the startup time to understand and optimize
startup performance. The ServerReady event in Microsoft.AspNetCore.Hosting
represents the point where the server is ready to respond to requests.
For more information on application startup, see ASP.NET Core fundamentals overview.

Extend Startup with startup filters


Use IStartupFilter:

To configure middleware at the beginning or end of an app's middleware pipeline


without an explicit call to Use{Middleware} . Use IStartupFilter to add defaults to
the beginning of the pipeline without explicitly registering the default middleware.
IStartupFilter allows a different component to call Use{Middleware} on behalf of

the app author.


To create a pipeline of Configure methods. IStartupFilter.Configure can set a
middleware to run before or after middleware added by libraries.

IStartupFilter implements Configure, which receives and returns an


Action<IApplicationBuilder> . An IApplicationBuilder defines a class to configure an

app's request pipeline. For more information, see Create a middleware pipeline with
IApplicationBuilder.

Each IStartupFilter can add one or more middlewares in the request pipeline. The
filters are invoked in the order they were added to the service container. Filters may add
middleware before or after passing control to the next filter, thus they append to the
beginning or end of the app pipeline.

The following example demonstrates how to register a middleware with IStartupFilter .


The RequestSetOptionsMiddleware middleware sets an options value from a query string
parameter:

C#

public class RequestSetOptionsMiddleware


{
private readonly RequestDelegate _next;

public RequestSetOptionsMiddleware(RequestDelegate next)


{
_next = next;
}

// Test with https://localhost:5001/Privacy/?option=Hello


public async Task Invoke(HttpContext httpContext)
{
var option = httpContext.Request.Query["option"];

if (!string.IsNullOrWhiteSpace(option))
{
httpContext.Items["option"] = WebUtility.HtmlEncode(option);
}

await _next(httpContext);
}
}

The RequestSetOptionsMiddleware is configured in the RequestSetOptionsStartupFilter


class:

C#

namespace WebStartup.Middleware;
// <snippet1>
public class RequestSetOptionsStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder>
next)
{
return builder =>
{
builder.UseMiddleware<RequestSetOptionsMiddleware>();
next(builder);
};
}
}
// </snippet1>

The IStartupFilter is registered in Program.cs :

C#

using WebStartup.Middleware;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddTransient<IStartupFilter,
RequestSetOptionsStartupFilter>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

When a query string parameter for option is provided, the middleware processes the
value assignment before the ASP.NET Core middleware renders the response:

CSHTML

@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p> Append query string ?option=hello</p>


Option String: @HttpContext.Items["option"];

Middleware execution order is set by the order of IStartupFilter registrations:

Multiple IStartupFilter implementations may interact with the same objects. If


ordering is important, order their IStartupFilter service registrations to match
the order that their middlewares should run.

Libraries may add middleware with one or more IStartupFilter implementations


that run before or after other app middleware registered with IStartupFilter . To
invoke an IStartupFilter middleware before a middleware added by a library's
IStartupFilter :

Position the service registration before the library is added to the service
container.
To invoke afterward, position the service registration after the library is added.

Note: You can't extend the ASP.NET Core app when you override Configure . For more
informaton, see this GitHub issue .

Add configuration at startup from an external


assembly
An IHostingStartup implementation allows adding enhancements to an app at startup
from an external assembly outside of the app's Program.cs file. For more information,
see Use hosting startup assemblies in ASP.NET Core.

Startup, ConfigureServices, and Configure


For information on using the ConfigureServices and Configure methods with the
minimal hosting model, see:

Use Startup with the minimal hosting model


The ASP.NET Core 5.0 version of this article:
The ConfigureServices method
The Configure method
Dependency injection in ASP.NET Core
Article • 05/09/2023

By Kirk Larkin , Steve Smith , and Brandon Dahler

ASP.NET Core supports the dependency injection (DI) software design pattern, which is a
technique for achieving Inversion of Control (IoC) between classes and their
dependencies.

For more information specific to dependency injection within MVC controllers, see
Dependency injection into controllers in ASP.NET Core.

For information on using dependency injection in applications other than web apps, see
Dependency injection in .NET.

For more information on dependency injection of options, see Options pattern in


ASP.NET Core.

This topic provides information on dependency injection in ASP.NET Core. The primary
documentation on using dependency injection is contained in Dependency injection in
.NET.

View or download sample code (how to download)

Overview of dependency injection


A dependency is an object that another object depends on. Examine the following
MyDependency class with a WriteMessage method that other classes depend on:

C#

public class MyDependency


{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message:
{message}");
}
}

A class can create an instance of the MyDependency class to make use of its WriteMessage
method. In the following example, the MyDependency class is a dependency of the
IndexModel class:
C#

public class IndexModel : PageModel


{
private readonly MyDependency _dependency = new MyDependency();

public void OnGet()


{
_dependency.WriteMessage("IndexModel.OnGet");
}
}

The class creates and directly depends on the MyDependency class. Code dependencies,
such as in the previous example, are problematic and should be avoided for the
following reasons:

To replace MyDependency with a different implementation, the IndexModel class


must be modified.
If MyDependency has dependencies, they must also be configured by the
IndexModel class. In a large project with multiple classes depending on

MyDependency , the configuration code becomes scattered across the app.


This implementation is difficult to unit test.

Dependency injection addresses these problems through:

The use of an interface or base class to abstract the dependency implementation.


Registration of the dependency in a service container. ASP.NET Core provides a
built-in service container, IServiceProvider. Services are typically registered in the
app's Program.cs file.
Injection of the service into the constructor of the class where it's used. The
framework takes on the responsibility of creating an instance of the dependency
and disposing of it when it's no longer needed.

In the sample app , the IMyDependency interface defines the WriteMessage method:

C#

public interface IMyDependency


{
void WriteMessage(string message);
}

This interface is implemented by a concrete type, MyDependency :


C#

public class MyDependency : IMyDependency


{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}

The sample app registers the IMyDependency service with the concrete type
MyDependency . The AddScoped method registers the service with a scoped lifetime, the

lifetime of a single request. Service lifetimes are described later in this topic.

C#

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

In the sample app, the IMyDependency service is requested and used to call the
WriteMessage method:

C#

public class Index2Model : PageModel


{
private readonly IMyDependency _myDependency;

public Index2Model(IMyDependency myDependency)


{
_myDependency = myDependency;
}

public void OnGet()


{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}

By using the DI pattern, the controller or Razor Page:


Doesn't use the concrete type MyDependency , only the IMyDependency interface it
implements. That makes it easy to change the implementation without modifying
the controller or Razor Page.
Doesn't create an instance of MyDependency , it's created by the DI container.

The implementation of the IMyDependency interface can be improved by using the built-
in logging API:

C#

public class MyDependency2 : IMyDependency


{
private readonly ILogger<MyDependency2> _logger;

public MyDependency2(ILogger<MyDependency2> logger)


{
_logger = logger;
}

public void WriteMessage(string message)


{
_logger.LogInformation( $"MyDependency2.WriteMessage Message:
{message}");
}
}

The updated Program.cs registers the new IMyDependency implementation:

C#

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

MyDependency2 depends on ILogger<TCategoryName>, which it requests in the


constructor. ILogger<TCategoryName> is a framework-provided service.

It's not unusual to use dependency injection in a chained fashion. Each requested
dependency in turn requests its own dependencies. The container resolves the
dependencies in the graph and returns the fully resolved service. The collective set of
dependencies that must be resolved is typically referred to as a dependency tree,
dependency graph, or object graph.

The container resolves ILogger<TCategoryName> by taking advantage of (generic) open


types, eliminating the need to register every (generic) constructed type.

In dependency injection terminology, a service:

Is typically an object that provides a service to other objects, such as the


IMyDependency service.
Is not related to a web service, although the service may use a web service.

The framework provides a robust logging system. The IMyDependency implementations


shown in the preceding examples were written to demonstrate basic DI, not to
implement logging. Most apps shouldn't need to write loggers. The following code
demonstrates using the default logging, which doesn't require any services to be
registered:

C#

public class AboutModel : PageModel


{
private readonly ILogger _logger;

public AboutModel(ILogger<AboutModel> logger)


{
_logger = logger;
}

public string Message { get; set; } = string.Empty;

public void OnGet()


{
Message = $"About page visited at
{DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation(Message);
}
}

Using the preceding code, there is no need to update Program.cs , because logging is
provided by the framework.

Register groups of services with extension


methods
The ASP.NET Core framework uses a convention for registering a group of related
services. The convention is to use a single Add{GROUP_NAME} extension method to register
all of the services required by a framework feature. For example, the AddControllers
extension method registers the services required for MVC controllers.

The following code is generated by the Razor Pages template using individual user
accounts and shows how to add additional services to the container using the extension
methods AddDbContext and AddDefaultIdentity:

C#

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Consider the following which registers services and configures options:

C#

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();
var app = builder.Build();

Related groups of registrations can be moved to an extension method to register


services. For example, the configuration services are added to the following class:

C#

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));

return services;
}

public static IServiceCollection AddMyDependencyGroup(


this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();

return services;
}
}
}

The remaining services are registered in a similar class. The following code uses the new
extension methods to register the services:

C#

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();

builder.Services.AddRazorPages();
var app = builder.Build();

Note: Each services.Add{GROUP_NAME} extension method adds and potentially configures


services. For example, AddControllersWithViews adds the services MVC controllers with
views require, and AddRazorPages adds the services Razor Pages requires.

Service lifetimes
See Service lifetimes in Dependency injection in .NET

To use scoped services in middleware, use one of the following approaches:

Inject the service into the middleware's Invoke or InvokeAsync method. Using
constructor injection throws a runtime exception because it forces the scoped
service to behave like a singleton. The sample in the Lifetime and registration
options section demonstrates the InvokeAsync approach.
Use Factory-based middleware. Middleware registered using this approach is
activated per client request (connection), which allows scoped services to be
injected into the middleware's constructor.

For more information, see Write custom ASP.NET Core middleware.

Service registration methods


See Service registration methods in Dependency injection in .NET

It's common to use multiple implementations when mocking types for testing.

Registering a service with only an implementation type is equivalent to registering that


service with the same implementation and service type. This is why multiple
implementations of a service cannot be registered using the methods that don't take an
explicit service type. These methods can register multiple instances of a service, but they
will all have the same implementation type.

Any of the above service registration methods can be used to register multiple service
instances of the same service type. In the following example, AddSingleton is called
twice with IMyDependency as the service type. The second call to AddSingleton overrides
the previous one when resolved as IMyDependency and adds to the previous one when
multiple services are resolved via IEnumerable<IMyDependency> . Services appear in the
order they were registered when resolved via IEnumerable<{SERVICE}> .
C#

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService


{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);

var dependencyArray = myDependencies.ToArray();


Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}

Constructor injection behavior


See Constructor injection behavior in Dependency injection in .NET

Entity Framework contexts


By default, Entity Framework contexts are added to the service container using the
scoped lifetime because web app database operations are normally scoped to the client
request. To use a different lifetime, specify the lifetime by using an AddDbContext
overload. Services of a given lifetime shouldn't use a database context with a lifetime
that's shorter than the service's lifetime.

Lifetime and registration options


To demonstrate the difference between service lifetimes and their registration options,
consider the following interfaces that represent a task as an operation with an identifier,
OperationId . Depending on how the lifetime of an operation's service is configured for
the following interfaces, the container provides either the same or different instances of
the service when requested by a class:

C#

public interface IOperation


{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

The following Operation class implements all of the preceding interfaces. The Operation
constructor generates a GUID and stores the last 4 characters in the OperationId
property:

C#

public class Operation : IOperationTransient, IOperationScoped,


IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}

public string OperationId { get; }


}

The following code creates multiple registrations of the Operation class according to the
named lifetimes:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();
app.MapRazorPages();

app.Run();

The sample app demonstrates object lifetimes both within and between requests. The
IndexModel and the middleware request each kind of IOperation type and log the
OperationId for each:

C#

public class IndexModel : PageModel


{
private readonly ILogger _logger;
private readonly IOperationTransient _transientOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationScoped _scopedOperation;

public IndexModel(ILogger<IndexModel> logger,


IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation)
{
_logger = logger;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
}

public void OnGet()


{
_logger.LogInformation("Transient: " +
_transientOperation.OperationId);
_logger.LogInformation("Scoped: " +
_scopedOperation.OperationId);
_logger.LogInformation("Singleton: " +
_singletonOperation.OperationId);
}
}

Similar to the IndexModel , the middleware resolves the same services:

C#

public class MyMiddleware


{
private readonly RequestDelegate _next;
private readonly ILogger _logger;

private readonly IOperationSingleton _singletonOperation;

public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,


IOperationSingleton singletonOperation)
{
_logger = logger;
_singletonOperation = singletonOperation;
_next = next;
}

public async Task InvokeAsync(HttpContext context,


IOperationTransient transientOperation, IOperationScoped
scopedOperation)
{
_logger.LogInformation("Transient: " +
transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " +
_singletonOperation.OperationId);

await _next(context);
}
}

public static class MyMiddlewareExtensions


{
public static IApplicationBuilder UseMyMiddleware(this
IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}

Scoped and transient services must be resolved in the InvokeAsync method:

C#

public async Task InvokeAsync(HttpContext context,


IOperationTransient transientOperation, IOperationScoped
scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

await _next(context);
}

The logger output shows:

Transient objects are always different. The transient OperationId value is different
in the IndexModel and in the middleware.
Scoped objects are the same for a given request but differ across each new request.
Singleton objects are the same for every request.
To reduce the logging output, set "Logging:LogLevel:Microsoft:Error" in the
appsettings.Development.json file:

JSON

{
"MyKey": "MyKey from appsettings.Developement.json",
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Debug",
"Microsoft": "Error"
}
}
}

Resolve a service at app start up


The following code shows how to resolve a scoped service for a limited duration when
the app starts:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())


{
var services = serviceScope.ServiceProvider;

var myDependency = services.GetRequiredService<IMyDependency>();


myDependency.WriteMessage("Call services from main");
}

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

app.Run();

Scope validation
See Constructor injection behavior in Dependency injection in .NET

For more information, see Scope validation.


Request Services
Services and their dependencies within an ASP.NET Core request are exposed through
HttpContext.RequestServices.

The framework creates a scope per request, and RequestServices exposes the scoped
service provider. All scoped services are valid for as long as the request is active.

7 Note

Prefer requesting dependencies as constructor parameters over resolving services


from RequestServices . Requesting dependencies as constructor parameters yields
classes that are easier to test.

Design services for dependency injection


When designing services for dependency injection:

Avoid stateful, static classes and members. Avoid creating global state by
designing apps to use singleton services instead.
Avoid direct instantiation of dependent classes within services. Direct instantiation
couples the code to a particular implementation.
Make services small, well-factored, and easily tested.

If a class has a lot of injected dependencies, it might be a sign that the class has too
many responsibilities and violates the Single Responsibility Principle (SRP). Attempt to
refactor the class by moving some of its responsibilities into new classes. Keep in mind
that Razor Pages page model classes and MVC controller classes should focus on UI
concerns.

Disposal of services
The container calls Dispose for the IDisposable types it creates. Services resolved from
the container should never be disposed by the developer. If a type or factory is
registered as a singleton, the container disposes the singleton automatically.

In the following example, the services are created by the service container and disposed
automatically: dependency-
injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

C#
public class Service1 : IDisposable
{
private bool _disposed;

public void Write(string message)


{
Console.WriteLine($"Service1: {message}");
}

public void Dispose()


{
if (_disposed)
return;

Console.WriteLine("Service1.Dispose");
_disposed = true;
}
}

public class Service2 : IDisposable


{
private bool _disposed;

public void Write(string message)


{
Console.WriteLine($"Service2: {message}");
}

public void Dispose()


{
if (_disposed)
return;

Console.WriteLine("Service2.Dispose");
_disposed = true;
}
}

public interface IService3


{
public void Write(string message);
}

public class Service3 : IService3, IDisposable


{
private bool _disposed;

public Service3(string myKey)


{
MyKey = myKey;
}

public string MyKey { get; }


public void Write(string message)
{
Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
}

public void Dispose()


{
if (_disposed)
return;

Console.WriteLine("Service3.Dispose");
_disposed = true;
}
}

C#

using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];


builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();

C#

public class IndexModel : PageModel


{
private readonly Service1 _service1;
private readonly Service2 _service2;
private readonly IService3 _service3;

public IndexModel(Service1 service1, Service2 service2, IService3


service3)
{
_service1 = service1;
_service2 = service2;
_service3 = service3;
}

public void OnGet()


{
_service1.Write("IndexModel.OnGet");
_service2.Write("IndexModel.OnGet");
_service3.Write("IndexModel.OnGet");
}
}

The debug console shows the following output after each refresh of the Index page:

Console

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Services not created by the service container


Consider the following code:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

In the preceding code:

The service instances aren't created by the service container.


The framework doesn't dispose of the services automatically.
The developer is responsible for disposing the services.

IDisposable guidance for Transient and shared instances


See IDisposable guidance for Transient and shared instance in Dependency injection in
.NET

Default service container replacement


See Default service container replacement in Dependency injection in .NET

Recommendations
See Recommendations in Dependency injection in .NET
Avoid using the service locator pattern. For example, don't invoke GetService to
obtain a service instance when you can use DI instead:

Incorrect:

Correct:

C#

public class MyClass


{
private readonly IOptionsMonitor<MyOptions> _optionsMonitor;

public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)


{
_optionsMonitor = optionsMonitor;
}

public void MyMethod()


{
var option = _optionsMonitor.CurrentValue.Option;

...
}
}

Another service locator variation to avoid is injecting a factory that resolves


dependencies at runtime. Both of these practices mix Inversion of Control
strategies.

Avoid static access to HttpContext (for example,


IHttpContextAccessor.HttpContext).

DI is an alternative to static/global object access patterns. You may not be able to realize
the benefits of DI if you mix it with static object access.
Recommended patterns for multi-tenancy in DI
Orchard Core is an application framework for building modular, multi-tenant
applications on ASP.NET Core. For more information, see the Orchard Core
Documentation .

See the Orchard Core samples for examples of how to build modular and multi-tenant
apps using just the Orchard Core Framework without any of its CMS-specific features.

Framework-provided services
Program.cs registers services that the app uses, including platform features, such as

Entity Framework Core and ASP.NET Core MVC. Initially, the IServiceCollection
provided to Program.cs has services defined by the framework depending on how the
host was configured. For apps based on the ASP.NET Core templates, the framework
registers more than 250 services.

The following table lists a small sample of these framework-registered services:

Service Type Lifetime

Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transient

IHostApplicationLifetime Singleton

IWebHostEnvironment Singleton

Microsoft.AspNetCore.Hosting.IStartup Singleton

Microsoft.AspNetCore.Hosting.IStartupFilter Transient

Microsoft.AspNetCore.Hosting.Server.IServer Singleton

Microsoft.AspNetCore.Http.IHttpContextFactory Transient

Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton

Microsoft.Extensions.Logging.ILoggerFactory Singleton

Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton

Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transient

Microsoft.Extensions.Options.IOptions<TOptions> Singleton

System.Diagnostics.DiagnosticSource Singleton

System.Diagnostics.DiagnosticListener Singleton
Additional resources
Dependency injection into views in ASP.NET Core
Dependency injection into controllers in ASP.NET Core
Dependency injection in requirement handlers in ASP.NET Core
ASP.NET Core Blazor dependency injection
NDC Conference Patterns for DI app development
App startup in ASP.NET Core
Factory-based middleware activation in ASP.NET Core
Four ways to dispose IDisposables in ASP.NET Core
Writing Clean Code in ASP.NET Core with Dependency Injection (MSDN)
Explicit Dependencies Principle
Inversion of Control Containers and the Dependency Injection Pattern (Martin
Fowler)
How to register a service with multiple interfaces in ASP.NET Core DI
ASP.NET Core Middleware
Article • 05/03/2023

By Rick Anderson and Steve Smith

Middleware is software that's assembled into an app pipeline to handle requests and
responses. Each component:

Chooses whether to pass the request to the next component in the pipeline.
Can perform work before and after the next component in the pipeline.

Request delegates are used to build the request pipeline. The request delegates handle
each HTTP request.

Request delegates are configured using Run, Map, and Use extension methods. An
individual request delegate can be specified in-line as an anonymous method (called in-
line middleware), or it can be defined in a reusable class. These reusable classes and in-
line anonymous methods are middleware, also called middleware components. Each
middleware component in the request pipeline is responsible for invoking the next
component in the pipeline or short-circuiting the pipeline. When a middleware short-
circuits, it's called a terminal middleware because it prevents further middleware from
processing the request.

Migrate HTTP handlers and modules to ASP.NET Core middleware explains the
difference between request pipelines in ASP.NET Core and ASP.NET 4.x and provides
additional middleware samples.

Middleware code analysis


ASP.NET Core includes many compiler platform analyzers that inspect application code
for quality. For more information, see Code analysis in ASP.NET Core apps

Create a middleware pipeline with


WebApplication
The ASP.NET Core request pipeline consists of a sequence of request delegates, called
one after the other. The following diagram demonstrates the concept. The thread of
execution follows the black arrows.
Each delegate can perform operations before and after the next delegate. Exception-
handling delegates should be called early in the pipeline, so they can catch exceptions
that occur in later stages of the pipeline.

The simplest possible ASP.NET Core app sets up a single request delegate that handles
all requests. This case doesn't include an actual request pipeline. Instead, a single
anonymous function is called in response to every HTTP request.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.Run(async context =>


{
await context.Response.WriteAsync("Hello world!");
});

app.Run();

Chain multiple request delegates together with Use. The next parameter represents the
next delegate in the pipeline. You can short-circuit the pipeline by not calling the next
parameter. You can typically perform actions both before and after the next delegate,
as the following example demonstrates:

C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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


{
// Do work that can write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});

app.Run();

When a delegate doesn't pass a request to the next delegate, it's called short-circuiting
the request pipeline. Short-circuiting is often desirable because it avoids unnecessary
work. For example, Static File Middleware can act as a terminal middleware by
processing a request for a static file and short-circuiting the rest of the pipeline.
Middleware added to the pipeline before the middleware that terminates further
processing still processes code after their next.Invoke statements. However, see the
following warning about attempting to write to a response that has already been sent.

2 Warning

Don't call next.Invoke after the response has been sent to the client. Changes to
HttpResponse after the response has started throw an exception. For example,
setting headers and a status code throw an exception. Writing to the response
body after calling next :

May cause a protocol violation. For example, writing more than the stated
Content-Length .

May corrupt the body format. For example, writing an HTML footer to a CSS
file.

HasStarted is a useful hint to indicate if headers have been sent or the body has
been written to.

Run delegates don't receive a next parameter. The first Run delegate is always terminal
and terminates the pipeline. Run is a convention. Some middleware components may
expose Run[Middleware] methods that run at the end of the pipeline:
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

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


{
// Do work that can write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});

app.Run();

If you would like to see code comments translated to languages other than English, let
us know in this GitHub discussion issue .

In the preceding example, the Run delegate writes "Hello from 2nd delegate." to the
response and then terminates the pipeline. If another Use or Run delegate is added
after the Run delegate, it's not called.

Prefer app.Use overload that requires passing the context


to next
The non-allocating app.Use extension method:

Requires passing the context to next .


Saves two internal per-request allocations that are required when using the other
overload.

For more information, see this GitHub issue .

Middleware order
The following diagram shows the complete request processing pipeline for ASP.NET
Core MVC and Razor Pages apps. You can see how, in a typical app, existing
middlewares are ordered and where custom middlewares are added. You have full
control over how to reorder existing middlewares or inject new custom middlewares as
necessary for your scenarios.
The Endpoint middleware in the preceding diagram executes the filter pipeline for the
corresponding app type—MVC or Razor Pages.

The Routing middleware in the preceding diagram is shown following Static Files. This is
the order that the project templates implement by explicitly calling app.UseRouting. If
you don't call app.UseRouting , the Routing middleware runs at the beginning of the
pipeline by default. For more information, see Routing.
The order that middleware components are added in the Program.cs file defines the
order in which the middleware components are invoked on requests and the reverse
order for the response. The order is critical for security, performance, and functionality.

The following highlighted code in Program.cs adds security-related middleware


components in the typical recommended order:

C#

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string
'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();


if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();

app.UseRouting();
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();

app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

In the preceding code:

Middleware that is not added when creating a new web app with individual users
accounts is commented out.
Not every middleware appears in this exact order, but many do. For example:
UseCors , UseAuthentication , and UseAuthorization must appear in the order

shown.
UseCors currently must appear before UseResponseCaching . This requirement is

explained in GitHub issue dotnet/aspnetcore #23218 .


UseRequestLocalization must appear before any middleware that might check

the request culture, for example, app.UseStaticFiles() .


UseRateLimiter must be called after UseRouting when rate limiting endpoint
specific APIs are used. For example, if the [EnableRateLimiting] attribute is used,
UseRateLimiter must be called after UseRouting . When calling only global
limiters, UseRateLimiter can be called before UseRouting .

In some scenarios, middleware has different ordering. For example, caching and
compression ordering is scenario specific, and there are multiple valid orderings. For
example:

C#

app.UseResponseCaching();
app.UseResponseCompression();

With the preceding code, CPU usage could be reduced by caching the compressed
response, but you might end up caching multiple representations of a resource using
different compression algorithms such as Gzip or Brotli.

The following ordering combines static files to allow caching compressed static files:

C#

app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

The following Program.cs code adds middleware components for common app
scenarios:

1. Exception/error handling

When the app runs in the Development environment:


Developer Exception Page Middleware (UseDeveloperExceptionPage)
reports app runtime errors.
Database Error Page Middleware (UseDatabaseErrorPage) reports database
runtime errors.
When the app runs in the Production environment:
Exception Handler Middleware (UseExceptionHandler) catches exceptions
thrown in the following middlewares.
HTTP Strict Transport Security Protocol (HSTS) Middleware (UseHsts) adds
the Strict-Transport-Security header.

2. HTTPS Redirection Middleware (UseHttpsRedirection) redirects HTTP requests to


HTTPS.
3. Static File Middleware (UseStaticFiles) returns static files and short-circuits further
request processing.
4. Cookie Policy Middleware (UseCookiePolicy) conforms the app to the EU General
Data Protection Regulation (GDPR) regulations.
5. Routing Middleware (UseRouting) to route requests.
6. Authentication Middleware (UseAuthentication) attempts to authenticate the user
before they're allowed access to secure resources.
7. Authorization Middleware (UseAuthorization) authorizes a user to access secure
resources.
8. Session Middleware (UseSession) establishes and maintains session state. If the
app uses session state, call Session Middleware after Cookie Policy Middleware and
before MVC Middleware.
9. Endpoint Routing Middleware (UseEndpoints with MapRazorPages) to add Razor
Pages endpoints to the request pipeline.

C#

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();

In the preceding example code, each middleware extension method is exposed on


WebApplicationBuilder through the Microsoft.AspNetCore.Builder namespace.

UseExceptionHandler is the first middleware component added to the pipeline.


Therefore, the Exception Handler Middleware catches any exceptions that occur in later
calls.

Static File Middleware is called early in the pipeline so that it can handle requests and
short-circuit without going through the remaining components. The Static File
Middleware provides no authorization checks. Any files served by Static File Middleware,
including those under wwwroot, are publicly available. For an approach to secure static
files, see Static files in ASP.NET Core.

If the request isn't handled by the Static File Middleware, it's passed on to the
Authentication Middleware (UseAuthentication), which performs authentication.
Authentication doesn't short-circuit unauthenticated requests. Although Authentication
Middleware authenticates requests, authorization (and rejection) occurs only after MVC
selects a specific Razor Page or MVC controller and action.

The following example demonstrates a middleware order where requests for static files
are handled by Static File Middleware before Response Compression Middleware. Static
files aren't compressed with this middleware order. The Razor Pages responses can be
compressed.

C#

// Static files aren't compressed by Static File Middleware.


app.UseStaticFiles();

app.UseRouting();

app.UseResponseCompression();

app.MapRazorPages();

For information about Single Page Applications, see the guides for the React and
Angular project templates.

UseCors and UseStaticFiles order


The order for calling UseCors and UseStaticFiles depends on the app. For more
information, see UseCors and UseStaticFiles order

Forwarded Headers Middleware order


Forwarded Headers Middleware should run before other middleware. This ordering
ensures that the middleware relying on forwarded headers information can consume the
header values for processing. To run Forwarded Headers Middleware after diagnostics
and error handling middleware, see Forwarded Headers Middleware order.

Branch the middleware pipeline


Map extensions are used as a convention for branching the pipeline. Map branches the
request pipeline based on matches of the given request path. If the request path starts
with the given path, the branch is executed.

C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1", HandleMapTest1);

app.Map("/map2", HandleMapTest2);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});

app.Run();

static void HandleMapTest1(IApplicationBuilder app)


{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}

static void HandleMapTest2(IApplicationBuilder app)


{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}

The following table shows the requests and responses from http://localhost:1234
using the preceding code.

Request Response

localhost:1234 Hello from non-Map delegate.

localhost:1234/map1 Map Test 1

localhost:1234/map2 Map Test 2

localhost:1234/map3 Hello from non-Map delegate.

When Map is used, the matched path segments are removed from HttpRequest.Path
and appended to HttpRequest.PathBase for each request.

Map supports nesting, for example:

C#
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});

Map can also match multiple segments at once:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.Map("/map1/seg1", HandleMultiSeg);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});

app.Run();

static void HandleMultiSeg(IApplicationBuilder app)


{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}

MapWhen branches the request pipeline based on the result of the given predicate. Any
predicate of type Func<HttpContext, bool> can be used to map requests to a new
branch of the pipeline. In the following example, a predicate is used to detect the
presence of a query string variable branch :

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapWhen(context => context.Request.Query.ContainsKey("branch"),


HandleBranch);

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});

app.Run();

static void HandleBranch(IApplicationBuilder app)


{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}

The following table shows the requests and responses from http://localhost:1234
using the previous code:

Request Response

localhost:1234 Hello from non-Map delegate.

localhost:1234/?branch=main Branch used = main

UseWhen also branches the request pipeline based on the result of the given predicate.
Unlike with MapWhen , this branch is rejoined to the main pipeline if it doesn't short-circuit
or contain a terminal middleware:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.UseWhen(context => context.Request.Query.ContainsKey("branch"),


appBuilder => HandleBranchAndRejoin(appBuilder));

app.Run(async context =>


{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});

app.Run();

void HandleBranchAndRejoin(IApplicationBuilder app)


{
var logger =
app.ApplicationServices.GetRequiredService<ILogger<Program>>();

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


{
var branchVer = context.Request.Query["branch"];
logger.LogInformation("Branch used = {branchVer}", branchVer);
// Do work that doesn't write to the Response.
await next();
// Do other work that doesn't write to the Response.
});
}

In the preceding example, a response of Hello from non-Map delegate. is written for all
requests. If the request includes a query string variable branch , its value is logged
before the main pipeline is rejoined.

Built-in middleware
ASP.NET Core ships with the following middleware components. The Order column
provides notes on middleware placement in the request processing pipeline and under
what conditions the middleware may terminate request processing. When a middleware
short-circuits the request processing pipeline and prevents further downstream
middleware from processing a request, it's called a terminal middleware. For more
information on short-circuiting, see the Create a middleware pipeline with
WebApplication section.

Middleware Description Order

Authentication Provides authentication Before HttpContext.User is needed.


support. Terminal for OAuth callbacks.

Authorization Provides authorization Immediately after the Authentication


support. Middleware.

Cookie Policy Tracks consent from Before middleware that issues cookies.
users for storing Examples: Authentication, Session, MVC
personal information (TempData).
and enforces minimum
standards for cookie
fields, such as secure
and SameSite .

CORS Configures Cross-Origin Before components that use CORS.


Resource Sharing. UseCors currently must go before
UseResponseCaching due to this bug .

DeveloperExceptionPage Generates a page with Before components that generate errors.


error information that is The project templates automatically
intended for use only in register this middleware as the first
the Development middleware in the pipeline when the
environment. environment is Development.
Middleware Description Order

Diagnostics Several separate Before components that generate errors.


middlewares that Terminal for exceptions or serving the
provide a developer default web page for new apps.
exception page,
exception handling,
status code pages, and
the default web page for
new apps.

Forwarded Headers Forwards proxied Before components that consume the


headers onto the current updated fields. Examples: scheme, host,
request. client IP, method.

Health Check Checks the health of an Terminal if a request matches a health


ASP.NET Core app and check endpoint.
its dependencies, such
as checking database
availability.

Header Propagation Propagates HTTP


headers from the
incoming request to the
outgoing HTTP Client
requests.

HTTP Logging Logs HTTP Requests and At the beginning of the middleware
Responses. pipeline.

HTTP Method Override Allows an incoming Before components that consume the
POST request to updated method.
override the method.

HTTPS Redirection Redirect all HTTP Before components that consume the URL.
requests to HTTPS.

HTTP Strict Transport Security enhancement Before responses are sent and after
Security (HSTS) middleware that adds a components that modify requests.
special response header. Examples: Forwarded Headers, URL
Rewriting.

MVC Processes requests with Terminal if a request matches a route.


MVC/Razor Pages.

OWIN Interop with OWIN- Terminal if the OWIN Middleware fully


based apps, servers, and processes the request.
middleware.
Middleware Description Order

Output Caching Provides support for Before components that require caching.
caching responses based UseRouting must come before
on configuration. UseOutputCaching . UseCORS must come
before UseOutputCaching .

Response Caching Provides support for Before components that require caching.
caching responses. This UseCORS must come before
requires client UseResponseCaching . Is typically not
participation to work. beneficial for UI apps such as Razor Pages
Use output caching for because browsers generally set request
complete server control. headers that prevent caching. Output
caching benefits UI apps.

Request Decompression Provides support for Before components that read the request
decompressing requests. body.

Response Compression Provides support for Before components that require


compressing responses. compression.

Request Localization Provides localization Before localization sensitive components.


support. Must appear after Routing Middleware
when using
RouteDataRequestCultureProvider.

Endpoint Routing Defines and constrains Terminal for matching routes.


request routes.

SPA Handles all requests Late in the chain, so that other middleware
from this point in the for serving static files, MVC actions, etc.,
middleware chain by takes precedence.
returning the default
page for the Single Page
Application (SPA)

Session Provides support for Before components that require Session.


managing user sessions.

Static Files Provides support for Terminal if a request matches a file.


serving static files and
directory browsing.

URL Rewrite Provides support for Before components that consume the URL.
rewriting URLs and
redirecting requests.

W3CLogging Generates server access At the beginning of the middleware


logs in the W3C pipeline.
Extended Log File
Format .
Middleware Description Order

WebSockets Enables the WebSockets Before components that are required to


protocol. accept WebSocket requests.

Additional resources
Lifetime and registration options contains a complete sample of middleware with
scoped, transient, and singleton lifetime services.
Write custom ASP.NET Core middleware
Test ASP.NET Core middleware
Configure gRPC-Web in ASP.NET Core
Migrate HTTP handlers and modules to ASP.NET Core middleware
App startup in ASP.NET Core
Request Features in ASP.NET Core
Factory-based middleware activation in ASP.NET Core
Middleware activation with a third-party container in ASP.NET Core
Rate limiting middleware in ASP.NET
Core
Article • 03/22/2023

By Arvin Kahbazi , Maarten Balliauw , and Rick Anderson

The Microsoft.AspNetCore.RateLimiting middleware provides rate limiting middleware.


Apps configure rate limiting policies and then attach the policies to endpoints. Apps
using rate limiting should be carefully load tested and reviewed before deploying. See
Testing endpoints with rate limiting in this article for more information.

Rate limiter algorithms


The RateLimiterOptionsExtensions class provides the following extension methods for
rate limiting:

Fixed window
Sliding window
Token bucket
Concurrency

Fixed window limiter


The AddFixedWindowLimiter method uses a fixed time window to limit requests. When
the time window expires, a new time window starts and the request limit is reset.

Consider the following code:

C#

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: "fixed", options =>
{
options.PermitLimit = 4;
options.Window = TimeSpan.FromSeconds(12);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 2;
}));
var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks &


0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))


.RequireRateLimiting("fixed");

app.Run();

The preceding code:

Calls AddRateLimiter to add a rate limiting service to the service collection.


Calls AddFixedWindowLimiter to create a fixed window limiter with a policy name of
"fixed" and sets:
PermitLimit to 4 and the time Window to 12. A maximum of 4 requests per each
12-second window are allowed.
QueueProcessingOrder to OldestFirst.
QueueLimit to 2.
Calls UseRateLimiter to enable rate limiting.

Apps should use Configuration to set limiter options. The following code updates the
preceding code using MyRateLimitOptions for configuration:

C#

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);


builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();


builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks &


0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Fixed Window Limiter {GetTicks()}"))


.RequireRateLimiting(fixedPolicy);

app.Run();

UseRateLimiter must be called after UseRouting when rate limiting endpoint specific
APIs are used. For example, if the [EnableRateLimiting] attribute is used, UseRateLimiter
must be called after UseRouting . When calling only global limiters, UseRateLimiter can
be called before UseRouting .

Sliding window limiter


A sliding window algorithm:

Is similar to the fixed window limiter but adds segments per window. The window
slides one segment each segment interval. The segment interval is (window
time)/(segments per window).

Limits the requests for a window to permitLimit requests.

Each time window is divided in n segments per window.

Requests taken from the expired time segment one window back ( n segments
prior to the current segment), are added to the current segment. We refer to the
most expired time segment one window back as the expired segment. Consider
the following table which shows a sliding window limiter with a 30-second window,
3 segments per window and a limit of 100 requests:

The top row and first column shows the time segment.

The second row shows the remaining requests available. The remaining requests
are available-requests+recycled.

Requests at each time moves along the diagonal blue line.

From time 30 on, the request taken from the expired time segment are added back
to the request limit, as shown in the red lines.
The following table shows the data in the previous graph in a different format. The
Remaining column shows the requests available from the previous segment (The Carry
over from the previous row). The first row shows 100 available because there's no
previous segment:

Time Available Taken Recycled from expired Carry over

0 100 20 0 80

10 80 30 0 50

20 50 40 0 10

30 10 30 20 0

40 0 10 30 20

50 20 10 40 50

60 50 35 30 45

The following code uses the sliding window rate limiter:

C#

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var myOptions = new MyRateLimitOptions();


builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);
var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks &


0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Sliding Window Limiter {GetTicks()}"))


.RequireRateLimiting(slidingPolicy);

app.Run();

Token bucket limiter


The token bucket limiter is similar to the sliding window limiter, but rather than adding
back the requests taken from the expired segment, a fixed number of tokens are added
each replenishment period. The tokens added each segment can't increase the available
tokens to a number higher than the token bucket limit. The following table shows a
token bucket limiter with a limit of 100 tokens and a 10-second replenishment period:

Time Available Taken Added Carry over

0 100 20 0 80

10 80 10 20 90

20 90 5 15 100

30 100 30 20 90

40 90 6 16 100

50 100 40 20 80

60 80 50 20 50

The following code uses the token bucket limiter:


C#

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var tokenPolicy = "token";


var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);

builder.Services.AddRateLimiter(_ => _
.AddTokenBucketLimiter(policyName: tokenPolicy, options =>
{
options.TokenLimit = myOptions.TokenLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
options.ReplenishmentPeriod =
TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod);
options.TokensPerPeriod = myOptions.TokensPerPeriod;
options.AutoReplenishment = myOptions.AutoReplenishment;
}));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks &


0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))


.RequireRateLimiting(tokenPolicy);

app.Run();

When AutoReplenishment is set to true , an internal timer replenishes the tokens every
ReplenishmentPeriod; when set to false , the app must call TryReplenish on the limiter.

Concurrency limiter
The concurrency limiter limits the number concurrent requests. Each request reduces the
concurrency limit by one. When a request completes, the limit is increased by one.
Unlike the other requests limiters that limit the total number of requests for a specified
period, the concurrency limiter limits only the number of concurrent requests and
doesn't cap the number of requests in a time period.

The following code uses the concurrency limiter:


C#

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var concurrencyPolicy = "Concurrency";


var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);

builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: concurrencyPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks &


0x11111).ToString("00000");

app.MapGet("/", async () =>


{
await Task.Delay(500);
return Results.Ok($"Concurrency Limiter {GetTicks()}");

}).RequireRateLimiting(concurrencyPolicy);

app.Run();

Create chained limiters


The CreateChained API allows passing in multiple PartitionedRateLimiter which are
combined into one PartitionedRateLimiter . The combined limiter runs all the input
limiters in sequence.

The following code uses CreateChained :

C#

using System.Globalization;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ =>
{
_.OnRejected = (context, _) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var
retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)
retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}

context.HttpContext.Response.StatusCode =
StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.WriteAsync("Too many requests. Please
try again later.");

return new ValueTask();


};
_.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent =
httpContext.Request.Headers.UserAgent.ToString();

return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 4,
Window = TimeSpan.FromSeconds(2)
});
}),
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent =
httpContext.Request.Headers.UserAgent.ToString();

return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 20,
Window = TimeSpan.FromSeconds(30)
});
}));
});

var app = builder.Build();


app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks &
0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"));

app.Run();

For more information, see the CreateChained source code

EnableRateLimiting and DisableRateLimiting


attributes
The [EnableRateLimiting] and [DisableRateLimiting] attributes can be applied to a
Controller, action method, or Razor Page. For Razor Pages, the attribute must be applied
to the Razor Page and not the page handlers. For example, [EnableRateLimiting] can't
be applied to OnGet , OnPost , or any other page handler.

The [DisableRateLimiting] attribute disables rate limiting to the Controller, action


method, or Razor Page regardless of named rate limiters or global limiters applied. For
example, consider the following code which calls RequireRateLimiting to apply the
fixedPolicy rate limiting to all controller endpoints:

C#

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();


builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));

var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));

var app = builder.Build();


app.UseRateLimiter();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);

app.Run();

In the following code, [DisableRateLimiting] disables rate limiting and overrides


[EnableRateLimiting("fixed")] applied to the Home2Controller and

app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) called in
Program.cs :

C#

[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;

public Home2Controller(ILogger<Home2Controller> logger)


{
_logger = logger;
}

public ActionResult Index()


{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}

[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None,


NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ??
HttpContext.TraceIdentifier });
}
}

In the preceding code, the [EnableRateLimiting("sliding")] is not applied to the


Privacy action method because Program.cs called

app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) .

Consider the following code which doesn't call RequireRateLimiting on MapRazorPages


or MapDefaultControllerRoute :

C#

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();


builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));

var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));

var app = builder.Build();

app.UseRateLimiter();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.MapRazorPages();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

Consider the following controller:

C#

[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;

public Home2Controller(ILogger<Home2Controller> logger)


{
_logger = logger;
}

public ActionResult Index()


{
return View();
}

[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}

[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None,


NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ??
HttpContext.TraceIdentifier });
}
}

In the preceding controller:

The "fixed" policy rate limiter is applied to all action methods that don't have
EnableRateLimiting and DisableRateLimiting attributes.
The "sliding" policy rate limiter is applied to the Privacy action.
Rate limiting is disabled on the NoLimit action method.

Applying attributes to Razor Pages


For Razor Pages, the attribute must be applied to the Razor Page and not the page
handlers. For example, [EnableRateLimiting] can't be applied to OnGet , OnPost , or any
other page handler.

The DisableRateLimiting attribute disables rate limiting on a Razor Page.


EnableRateLimiting is only applied to a Razor Page if
MapRazorPages().RequireRateLimiting(Policy) has not been called.

Limiter algorithm comparison


The fixed, sliding, and token limiters all limit the maximum number of requests in a time
period. The concurrency limiter limits only the number of concurrent requests and
doesn't cap the number of requests in a time period. The cost of an endpoint should be
considered when selecting a limiter. The cost of an endpoint includes the resources
used, for example, time, data access, CPU, and I/O.

Rate limiter samples


The following samples aren't meant for production code but are examples on how to
use the limiters.

Limiter with OnRejected , RetryAfter , and GlobalLimiter


The following sample:

Creates a RateLimiterOptions.OnRejected callback that is called when a request


exceeds the specified limit. retryAfter can be used with the
TokenBucketRateLimiter , FixedWindowLimiter , and SlidingWindowLimiter
because these algorithms are able to estimate when more permits will be added.
The ConcurrencyLimiter has no way of calculating when permits will be available.

Adds the following limiters:


A SampleRateLimiterPolicy which implements the
IRateLimiterPolicy<TPartitionKey> interface. The SampleRateLimiterPolicy

class is shown later in this article.


A SlidingWindowLimiter :
With a partition for each authenticated user.
One shared partition for all anonymous users.
A GlobalLimiter that is applied to all requests. The global limiter will be executed
first, followed by the endpoint-specific limiter, if one exists. The GlobalLimiter
creates a partition for each IPAddress.

C#

// Preceding code removed for brevity.


using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);


var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection") ??
throw new InvalidOperationException("Connection string
'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var userPolicyName = "user";


var helloPolicy = "hello";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);

builder.Services.AddRateLimiter(limiterOptions =>
{
limiterOptions.OnRejected = (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var
retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)
retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}

context.HttpContext.Response.StatusCode =
StatusCodes.Status429TooManyRequests;
context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
.CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
.LogWarning("OnRejected: {GetUserEndPoint}",
GetUserEndPoint(context.HttpContext));

return new ValueTask();


};

limiterOptions.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy);
limiterOptions.AddPolicy(userPolicyName, context =>
{
var username = "anonymous user";
if (context.User.Identity?.IsAuthenticated is true)
{
username = context.User.ToString()!;
}
return RateLimitPartition.GetSlidingWindowLimiter(username,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = myOptions.PermitLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
Window = TimeSpan.FromSeconds(myOptions.Window),
SegmentsPerWindow = myOptions.SegmentsPerWindow
});

});

limiterOptions.GlobalLimiter =
PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>
{
IPAddress? remoteIpAddress = context.Connection.RemoteIpAddress;

if (!IPAddress.IsLoopback(remoteIpAddress!))
{
return RateLimitPartition.GetTokenBucketLimiter
(remoteIpAddress!, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod =
TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}

return RateLimitPartition.GetNoLimiter(IPAddress.Loopback);
});
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages().RequireRateLimiting(userPolicyName);
app.MapDefaultControllerRoute();

static string GetUserEndPoint(HttpContext context) =>


$"User {context.User.Identity?.Name ?? "Anonymous"} endpoint:
{context.Request.Path}"
+ $" {context.Connection.RemoteIpAddress}";
static string GetTicks() => (DateTime.Now.Ticks &
0x11111).ToString("00000");

app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)}


{GetTicks()}")
.RequireRateLimiting(userPolicyName);

app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)}


{GetTicks()}")
.RequireRateLimiting(helloPolicy);

app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)}


{GetTicks()}");

app.Run();

2 Warning

Creating partitions on client IP addresses makes the app vulnerable to Denial of


Service Attacks which employ IP Source Address Spoofing. For more information,
see BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service
Attacks which employ IP Source Address Spoofing .

See the samples repository for the complete Program.cs file.

The SampleRateLimiterPolicy class

C#

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using WebRateLimitAuth.Models;

namespace WebRateLimitAuth;

public class SampleRateLimiterPolicy : IRateLimiterPolicy<string>


{
private Func<OnRejectedContext, CancellationToken, ValueTask>?
_onRejected;
private readonly MyRateLimitOptions _options;

public SampleRateLimiterPolicy(ILogger<SampleRateLimiterPolicy> logger,


IOptions<MyRateLimitOptions> options)
{
_onRejected = (ctx, token) =>
{
ctx.HttpContext.Response.StatusCode =
StatusCodes.Status429TooManyRequests;
logger.LogWarning($"Request rejected by
{nameof(SampleRateLimiterPolicy)}");
return ValueTask.CompletedTask;
};
_options = options.Value;
}

public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected


=> _onRejected;

public RateLimitPartition<string> GetPartition(HttpContext httpContext)


{
return RateLimitPartition.GetSlidingWindowLimiter(string.Empty,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = _options.PermitLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = _options.QueueLimit,
Window = TimeSpan.FromSeconds(_options.Window),
SegmentsPerWindow = _options.SegmentsPerWindow
});
}
}

In the preceding code, OnRejected uses OnRejectedContext to set the response status
to 429 Too Many Requests . The default rejected status is 503 Service Unavailable .

Limiter with authorization


The following sample uses JSON Web Tokens (JWT) and creates a partition with the JWT
access token . In a production app, the JWT would typically be provided by a server
acting as a Security token service (STS). For local development, the dotnet user-jwts
command line tool can be used to create and manage app-specific local JWTs.

C#

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Primitives;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

var myOptions = new MyRateLimitOptions();


builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);
var jwtPolicyName = "jwt";

builder.Services.AddRateLimiter(limiterOptions =>
{
limiterOptions.RejectionStatusCode =
StatusCodes.Status429TooManyRequests;
limiterOptions.AddPolicy(policyName: jwtPolicyName, partitioner:
httpContext =>
{
var accessToken =
httpContext.Features.Get<IAuthenticateResultFeature>()?

.AuthenticateResult?.Properties?.GetTokenValue("access_token")?.ToString()
?? string.Empty;

if (!StringValues.IsNullOrEmpty(accessToken))
{
return RateLimitPartition.GetTokenBucketLimiter(accessToken, _
=>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod =
TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}

return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>


new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod =
TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = true
});
});
});

var app = builder.Build();


app.UseAuthorization();
app.UseRateLimiter();

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

app.MapGet("/jwt", (HttpContext context) => $"Hello


{GetUserEndPointMethod(context)}")
.RequireRateLimiting(jwtPolicyName)
.RequireAuthorization();

app.MapPost("/post", (HttpContext context) => $"Hello


{GetUserEndPointMethod(context)}")
.RequireRateLimiting(jwtPolicyName)
.RequireAuthorization();

app.Run();

static string GetUserEndPointMethod(HttpContext context) =>


$"Hello {context.User.Identity?.Name ?? "Anonymous"} " +
$"Endpoint:{context.Request.Path} Method: {context.Request.Method}";

Limiter with ConcurrencyLimiter , TokenBucketRateLimiter ,


and authorization
The following sample:

Adds a ConcurrencyLimiter with a policy name of "get" that is used on the Razor
Pages.
Adds a TokenBucketRateLimiter with a partition for each authorized user and a
partition for all anonymous users.
Sets RateLimiterOptions.RejectionStatusCode to 429 Too Many Requests .

C#

var getPolicyName = "get";


var postPolicyName = "post";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOpti
ons);

builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: getPolicyName, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
})
.AddPolicy(policyName: postPolicyName, partitioner: httpContext =>
{
string userName = httpContext.User.Identity?.Name ?? string.Empty;

if (!StringValues.IsNullOrEmpty(userName))
{
return RateLimitPartition.GetTokenBucketLimiter(userName, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod =
TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}

return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>


new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod =
TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = true
});
}));

See the samples repository for the complete Program.cs file.

Testing endpoints with rate limiting


Before deploying an app using rate limiting to production, stress test the app to validate
the rate limiters and options used. For example, create a JMeter script with a tool like
BlazeMeter or Apache JMeter HTTP(S) Test Script Recorder and load the script to
Azure Load Testing.

Creating partitions with user input makes the app vulnerable to Denial of Service
(DoS) Attacks. For example, creating partitions on client IP addresses makes the app
vulnerable to Denial of Service Attacks that employ IP Source Address Spoofing. For
more information, see BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of
Service Attacks that employ IP Source Address Spoofing .

Additional resources
Rate limiting middleware by Maarten Balliauw
Rate limit an HTTP handler in .NET
Middleware in Minimal API apps
Article • 04/19/2023

WebApplication automatically adds the following middleware depending on certain


conditions:

UseDeveloperExceptionPage is added first when the HostingEnvironment is


"Development" .

UseRouting is added second if user code didn't already call UseRouting and if there
are endpoints configured, for example app.MapGet .
UseEndpoints is added at the end of the middleware pipeline if any endpoints are
configured.
UseAuthentication is added immediately after UseRouting if
IAuthenticationSchemeProvider can be detected in the service provider.
IAuthenticationSchemeProvider is added by default when using
AddAuthentication, and services are detected using IServiceProviderIsService.
UseAuthorization is added next if IAuthorizationHandlerProvider can be detected
in the service provider. IAuthorizationHandlerProvider is added by default when
using AddAuthorization, and services are detected using
IServiceProviderIsService .
User configured middleware and endpoints are added between UseRouting and
UseEndpoints .

The following code is effectively what the automatic middleware being added to the app
produces:

C#

if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
app.UseAuthorization();
}
// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

In some cases, the default middleware configuration isn't correct for the app and
requires modification. For example, UseCors should be called before UseAuthentication
and UseAuthorization. The app needs to call UseAuthentication and UseAuthorization if
UseCors is called:

C#

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

If middleware should be run before route matching occurs, UseRouting should be called
and the middleware should be placed before the call to UseRouting . UseEndpoints isn't
required in this case as it is automatically added as described previously:

C#

app.Use((context, next) =>


{
return next(context);
});

app.UseRouting();

// other middleware and endpoints

When adding a terminal middleware:

The middleware must be added after UseEndpoints .


The app needs to call UseRouting and UseEndpoints so that the terminal
middleware can be placed at the correct location.

C#

app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});


app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});

Terminal middleware is middleware that runs if no endpoint handles the request.

For more information about middleware see ASP.NET Core Middleware, and the list of
built-in middleware that can be added to applications.
Test ASP.NET Core middleware
Article • 06/03/2022

By Chris Ross

Middleware can be tested in isolation with TestServer. It allows you to:

Instantiate an app pipeline containing only the components that you need to test.
Send custom requests to verify middleware behavior.

Advantages:

Requests are sent in-memory rather than being serialized over the network.
This avoids additional concerns, such as port management and HTTPS certificates.
Exceptions in the middleware can flow directly back to the calling test.
It's possible to customize server data structures, such as HttpContext, directly in
the test.

Set up the TestServer


In the test project, create a test:

Build and start a host that uses TestServer.

Add any required services that the middleware uses.

Add a package reference to the project for the Microsoft.AspNetCore.TestHost


NuGet package.

Configure the processing pipeline to use the middleware for the test.

C#

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
using var host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddMyServices();
})
.Configure(app =>
{
app.UseMiddleware<MyMiddleware>();
});
})
.StartAsync();

...
}

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 .

Send requests with HttpClient


Send a request using HttpClient:

C#

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
using var host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddMyServices();
})
.Configure(app =>
{
app.UseMiddleware<MyMiddleware>();
});
})
.StartAsync();
var response = await host.GetTestClient().GetAsync("/");

...
}

Assert the result. First, make an assertion the opposite of the result that you expect. An
initial run with a false positive assertion confirms that the test fails when the middleware
is performing correctly. Run the test and confirm that the test fails.

In the following example, the middleware should return a 404 status code (Not Found)
when the root endpoint is requested. Make the first test run with Assert.NotEqual( ...
); , which should fail:

C#

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
using var host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddMyServices();
})
.Configure(app =>
{
app.UseMiddleware<MyMiddleware>();
});
})
.StartAsync();

var response = await host.GetTestClient().GetAsync("/");

Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode);
}

Change the assertion to test the middleware under normal operating conditions. The
final test uses Assert.Equal( ... ); . Run the test again to confirm that it passes.

C#

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
using var host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddMyServices();
})
.Configure(app =>
{
app.UseMiddleware<MyMiddleware>();
});
})
.StartAsync();

var response = await host.GetTestClient().GetAsync("/");

Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

Send requests with HttpContext


A test app can also send a request using SendAsync(Action<HttpContext>,
CancellationToken). In the following example, several checks are made when
https://example.com/A/Path/?and=query is processed by the middleware:

C#

[Fact]
public async Task TestMiddleware_ExpectedResponse()
{
using var host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddMyServices();
})
.Configure(app =>
{
app.UseMiddleware<MyMiddleware>();
});
})
.StartAsync();

var server = host.GetTestServer();


server.BaseAddress = new Uri("https://example.com/A/Path/");

var context = await server.SendAsync(c =>


{
c.Request.Method = HttpMethods.Post;
c.Request.Path = "/and/file.txt";
c.Request.QueryString = new QueryString("?and=query");
});

Assert.True(context.RequestAborted.CanBeCanceled);
Assert.Equal(HttpProtocol.Http11, context.Request.Protocol);
Assert.Equal("POST", context.Request.Method);
Assert.Equal("https", context.Request.Scheme);
Assert.Equal("example.com", context.Request.Host.Value);
Assert.Equal("/A/Path", context.Request.PathBase.Value);
Assert.Equal("/and/file.txt", context.Request.Path.Value);
Assert.Equal("?and=query", context.Request.QueryString.Value);
Assert.NotNull(context.Request.Body);
Assert.NotNull(context.Request.Headers);
Assert.NotNull(context.Response.Headers);
Assert.NotNull(context.Response.Body);
Assert.Equal(404, context.Response.StatusCode);
Assert.Null(context.Features.Get<IHttpResponseFeature>().ReasonPhrase);
}

SendAsync permits direct configuration of an HttpContext object rather than using the
HttpClient abstractions. Use SendAsync to manipulate structures only available on the
server, such as HttpContext.Items or HttpContext.Features.

As with the earlier example that tested for a 404 - Not Found response, check the
opposite for each Assert statement in the preceding test. The check confirms that the
test fails correctly when the middleware is operating normally. After you've confirmed
that the false positive test works, set the final Assert statements for the expected
conditions and values of the test. Run it again to confirm that the test passes.

TestServer limitations
TestServer:

Was created to replicate server behaviors to test middleware.


Does not try to replicate all HttpClient behaviors.
Attempts to give the client access to as much control over the server as possible,
and with as much visibility into what's happening on the server as possible. For
example it may throw exceptions not normally thrown by HttpClient in order to
directly communicate server state.
Doesn't set some transport specific headers by default as those aren't usually
relevant to middleware. For more information, see the next section.
Ignores the Stream position passed through StreamContent. HttpClient sends the
entire stream from the start position, even when positioning is set. For more
information, see this GitHub issue .

Content-Length and Transfer-Encoding headers


TestServer does not set transport related request or response headers such as Content-
Length or Transfer-Encoding . Applications should avoid depending on these
headers because their usage varies by client, scenario, and protocol. If Content-Length
and Transfer-Encoding are necessary to test a specific scenario, they can be specified in
the test when composing the HttpRequestMessage or HttpContext. For more
information, see the following GitHub issues:

dotnet/aspnetcore#21677
dotnet/aspnetcore#18463
dotnet/aspnetcore#13273
Response Caching Middleware in
ASP.NET Core
Article • 04/11/2023

By John Luo and Rick Anderson

This article explains how to configure Response Caching Middleware in an ASP.NET


Core app. The middleware determines when responses are cacheable, stores responses,
and serves responses from cache. For an introduction to HTTP caching and the
[ResponseCache] attribute, see Response Caching.

The Response caching middleware:

Enables caching server responses based on HTTP cache headers . Implements the
standard HTTP caching semantics. Caches based on HTTP cache headers like
proxies do.
Is typically not beneficial for UI apps such as Razor Pages because browsers
generally set request headers that prevent caching. Output caching, which is
available in ASP.NET Core 7.0 and later, benefits UI apps. With output caching,
configuration decides what should be cached independently of HTTP headers.
May be beneficial for public GET or HEAD API requests from clients where the
Conditions for caching are met.

To test response caching, use Fiddler , Postman , or another tool that can explicitly
set request headers. Setting headers explicitly is preferred for testing caching. For more
information, see Troubleshooting.

Configuration
In Program.cs , add the Response Caching Middleware services AddResponseCaching to
the service collection and configure the app to use the middleware with the
UseResponseCaching extension method. UseResponseCaching adds the middleware to
the request processing pipeline:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching();

var app = builder.Build();


app.UseHttpsRedirection();

// UseCors must be called before UseResponseCaching


//app.UseCors();

app.UseResponseCaching();

2 Warning

UseCors must be called before UseResponseCaching when using CORS


middleware.

The sample app adds headers to control caching on subsequent requests:

Cache-Control : Caches cacheable responses for up to 10 seconds.


Vary : Configures the middleware to serve a cached response only if the Accept-
Encoding header of subsequent requests matches that of the original request.

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseHttpsRedirection();

// UseCors must be called before UseResponseCaching


//app.UseCors();

app.UseResponseCaching();

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


{
context.Response.GetTypedHeaders().CacheControl =
new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
new string[] { "Accept-Encoding" };

await next();
});

app.MapGet("/", () => DateTime.Now.Millisecond);

app.Run();
The preceding headers are not written to the response and are overridden when a
controller, action, or Razor Page:

Has a [ResponseCache] attribute. This applies even if a property isn't set. For
example, omitting the VaryByHeader property will cause the corresponding header
to be removed from the response.

Response Caching Middleware only caches server responses that result in a 200 (OK)
status code. Any other responses, including error pages, are ignored by the middleware.

2 Warning

Responses containing content for authenticated clients must be marked as not


cacheable to prevent the middleware from storing and serving those responses.
See Conditions for caching for details on how the middleware determines if a
response is cacheable.

The preceding code typically doesn't return a cached value to a browser. Use Fiddler ,
Postman , or another tool that can explicitly set request headers and is preferred for
testing caching. For more information, see Troubleshooting in this article.

Options
Response caching options are shown in the following table.

Option Description

MaximumBodySize The largest cacheable size for the response body in bytes. The default
value is 64 * 1024 * 1024 (64 MB).

SizeLimit The size limit for the response cache middleware in bytes. The default
value is 100 * 1024 * 1024 (100 MB).

UseCaseSensitivePaths Determines if responses are cached on case-sensitive paths. The default


value is false .

The following example configures the middleware to:

Cache responses with a body size smaller than or equal to 1,024 bytes.
Store the responses by case-sensitive paths. For example, /page1 and /Page1 are
stored separately.
C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching(options =>
{
options.MaximumBodySize = 1024;
options.UseCaseSensitivePaths = true;
});

var app = builder.Build();

app.UseHttpsRedirection();

// UseCors must be called before UseResponseCaching


//app.UseCors();

app.UseResponseCaching();

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


{
context.Response.GetTypedHeaders().CacheControl =
new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
new string[] { "Accept-Encoding" };

await next(context);
});

app.MapGet("/", () => DateTime.Now.Millisecond);

app.Run();

VaryByQueryKeys
When using MVC, web API controllers, or Razor Pages page models, the
[ResponseCache] attribute specifies the parameters necessary for setting the
appropriate headers for response caching. The only parameter of the [ResponseCache]
attribute that strictly requires the middleware is VaryByQueryKeys, which doesn't
correspond to an actual HTTP header. For more information, see Response caching in
ASP.NET Core.

When not using the [ResponseCache] attribute, response caching can be varied with
VaryByQueryKeys . Use the ResponseCachingFeature directly from the
HttpContext.Features:
C#

var responseCachingFeature =
context.HttpContext.Features.Get<IResponseCachingFeature>();

if (responseCachingFeature != null)
{
responseCachingFeature.VaryByQueryKeys = new[] { "MyKey" };
}

Using a single value equal to * in VaryByQueryKeys varies the cache by all request query
parameters.

HTTP headers used by Response Caching


Middleware
The following table provides information on HTTP headers that affect response caching.

Header Details

Authorization The response isn't cached if the header exists.

Cache-Control The middleware only considers caching responses marked with the public cache
directive. Control caching with the following parameters:

max-age
max-stale†
min-fresh
must-revalidate
no-cache
no-store
only-if-cached
private
public
s-maxage
proxy-revalidate‡

†If no limit is specified to max-stale , the middleware takes no action.


‡ proxy-revalidate has the same effect as must-revalidate .

For more information, see RFC 9111: Request Directives .

Pragma A Pragma: no-cache header in the request produces the same effect as Cache-
Control: no-cache . This header is overridden by the relevant directives in the
Cache-Control header, if present. Considered for backward compatibility with
HTTP/1.0.
Header Details

Set-Cookie The response isn't cached if the header exists. Any middleware in the request
processing pipeline that sets one or more cookies prevents the Response
Caching Middleware from caching the response (for example, the cookie-based
TempData provider).

Vary The Vary header is used to vary the cached response by another header. For
example, cache responses by encoding by including the Vary: Accept-Encoding
header, which caches responses for requests with headers Accept-Encoding:
gzip and Accept-Encoding: text/plain separately. A response with a header
value of * is never stored.

Expires A response deemed stale by this header isn't stored or retrieved unless
overridden by other Cache-Control headers.

If-None-Match The full response is served from cache if the value isn't * and the ETag of the
response doesn't match any of the values provided. Otherwise, a 304 (Not
Modified) response is served.

If-Modified- If the If-None-Match header isn't present, a full response is served from cache if
Since the cached response date is newer than the value provided. Otherwise, a 304 -
Not Modified response is served.

Date When serving from cache, the Date header is set by the middleware if it wasn't
provided on the original response.

Content- When serving from cache, the Content-Length header is set by the middleware if
Length it wasn't provided on the original response.

Age The Age header sent in the original response is ignored. The middleware
computes a new value when serving a cached response.

Caching respects request Cache-Control


directives
The middleware respects the rules of RFC 9111: HTTP Caching (Section 5.2. Cache-
Control) . The rules require a cache to honor a valid Cache-Control header sent by the
client. Under the specification, a client can make requests with a no-cache header value
and force the server to generate a new response for every request. Currently, there's no
developer control over this caching behavior when using the middleware because the
middleware adheres to the official caching specification.

For more control over caching behavior, explore other caching features of ASP.NET
Core. See the following topics:
Cache in-memory in ASP.NET Core
Distributed caching in ASP.NET Core
Cache Tag Helper in ASP.NET Core MVC
Distributed Cache Tag Helper in ASP.NET Core

Troubleshooting
The Response Caching Middleware uses IMemoryCache, which has a limited capacity.
When the capacity is exceeded, the memory cache is compacted .

If caching behavior isn't as expected, confirm that responses are cacheable and capable
of being served from the cache. Examine the request's incoming headers and the
response's outgoing headers. Enable logging to help with debugging.

When testing and troubleshooting caching behavior, a browser typically sets request
headers that prevent caching. For example, a browser may set the Cache-Control header
to no-cache or max-age=0 when refreshing a page. Fiddler , Postman , and other tools
can explicitly set request headers and are preferred for testing caching.

Conditions for caching


The request must result in a server response with a 200 (OK) status code.
The request method must be GET or HEAD.
Response Caching Middleware must be placed before middleware that require
caching. For more information, see ASP.NET Core Middleware.
The Authorization header must not be present.
Cache-Control header parameters must be valid, and the response must be

marked public and not marked private .


The Pragma: no-cache header must not be present if the Cache-Control header
isn't present, as the Cache-Control header overrides the Pragma header when
present.
The Set-Cookie header must not be present.
Vary header parameters must be valid and not equal to * .
The Content-Length header value (if set) must match the size of the response
body.
The IHttpSendFileFeature isn't used.
The response must not be stale as specified by the Expires header and the max-
age and s-maxage cache directives.
Response buffering must be successful. The size of the response must be smaller
than the configured or default SizeLimit. The body size of the response must be
smaller than the configured or default MaximumBodySize.
The response must be cacheable according to RFC 9111: HTTP Caching . For
example, the no-store directive must not exist in request or response header
fields. See RFC 9111: HTTP Caching (Section 3: Storing Responses in Caches for
details.

7 Note

The Antiforgery system for generating secure tokens to prevent Cross-Site Request
Forgery (CSRF) attacks sets the Cache-Control and Pragma headers to no-cache so
that responses aren't cached. For information on how to disable antiforgery tokens
for HTML form elements, see Prevent Cross-Site Request Forgery (XSRF/CSRF)
attacks in ASP.NET Core.

Additional resources
View or download sample code (how to download)
GitHub source for IResponseCachingPolicyProvider
GitHub source for IResponseCachingPolicyProvider
App startup in ASP.NET Core
ASP.NET Core Middleware
Cache in-memory in ASP.NET Core
Distributed caching in ASP.NET Core
Detect changes with change tokens in ASP.NET Core
Response caching in ASP.NET Core
Cache Tag Helper in ASP.NET Core MVC
Distributed Cache Tag Helper in ASP.NET Core
Write custom ASP.NET Core middleware
Article • 06/03/2022

By Fiyaz Hasan , Rick Anderson , and Steve Smith

Middleware is software that's assembled into an app pipeline to handle requests and
responses. ASP.NET Core provides a rich set of built-in middleware components, but in
some scenarios you might want to write a custom middleware.

This topic describes how to write convention-based middleware. For an approach that
uses strong typing and per-request activation, see Factory-based middleware activation
in ASP.NET Core.

Middleware class
Middleware is generally encapsulated in a class and exposed with an extension method.
Consider the following inline middleware, which sets the culture for the current request
from a query string:

C#

using System.Globalization;

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.UseHttpsRedirection();

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


{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);

CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}

// Call the next delegate/middleware in the pipeline.


await next(context);
});

app.Run(async (context) =>


{
await context.Response.WriteAsync(
$"CurrentCulture.DisplayName:
{CultureInfo.CurrentCulture.DisplayName}");
});

app.Run();

The preceding highlighted inline middleware is used to demonstrate creating a


middleware component by calling Microsoft.AspNetCore.Builder.UseExtensions.Use. The
preceding Use extension method adds a middleware delegate defined in-line to the
application's request pipeline.

There are two overloads available for the Use extension:

One takes a HttpContext and a Func<Task> . Invoke the Func<Task> without any
parameters.
The other takes a HttpContext and a RequestDelegate. Invoke the RequestDelegate
by passing the HttpContext .

Prefer using the later overload as it saves two internal per-request allocations that are
required when using the other overload.

Test the middleware by passing in the culture. For example, request


https://localhost:5001/?culture=es-es .

For ASP.NET Core's built-in localization support, see Globalization and localization in
ASP.NET Core.

The following code moves the middleware delegate to a class:

C#

using System.Globalization;

namespace Middleware.Example;

public class RequestCultureMiddleware


{
private readonly RequestDelegate _next;

public RequestCultureMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task InvokeAsync(HttpContext context)


{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);

CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}

// Call the next delegate/middleware in the pipeline.


await _next(context);
}
}

The middleware class must include:

A public constructor with a parameter of type RequestDelegate.


A public method named Invoke or InvokeAsync . This method must:
Return a Task .
Accept a first parameter of type HttpContext.

Additional parameters for the constructor and Invoke / InvokeAsync are populated by
dependency injection (DI).

Typically, an extension method is created to expose the middleware through


IApplicationBuilder:

C#

using System.Globalization;

namespace Middleware.Example;

public class RequestCultureMiddleware


{
private readonly RequestDelegate _next;

public RequestCultureMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task InvokeAsync(HttpContext context)


{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);

CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}

// Call the next delegate/middleware in the pipeline.


await _next(context);
}
}

public static class RequestCultureMiddlewareExtensions


{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}

The following code calls the middleware from Program.cs :

C#

using Middleware.Example;
using System.Globalization;

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.UseHttpsRedirection();

app.UseRequestCulture();

app.Run(async (context) =>


{
await context.Response.WriteAsync(
$"CurrentCulture.DisplayName:
{CultureInfo.CurrentCulture.DisplayName}");
});

app.Run();

Middleware dependencies
Middleware should follow the Explicit Dependencies Principle by exposing its
dependencies in its constructor. Middleware is constructed once per application lifetime.

Middleware components can resolve their dependencies from dependency injection (DI)
through constructor parameters. UseMiddleware can also accept additional parameters
directly.

Per-request middleware dependencies


Middleware is constructed at app startup and therefore has application life time. Scoped
lifetime services used by middleware constructors aren't shared with other dependency-
injected types during each request. To share a scoped service between middleware and
other types, add these services to the InvokeAsync method's signature. The InvokeAsync
method can accept additional parameters that are populated by DI:

C#

namespace Middleware.Example;

public class MyCustomMiddleware


{
private readonly RequestDelegate _next;

public MyCustomMiddleware(RequestDelegate next)


{
_next = next;
}

// IMessageWriter is injected into InvokeAsync


public async Task InvokeAsync(HttpContext httpContext, IMessageWriter
svc)
{
svc.Write(DateTime.Now.Ticks.ToString());
await _next(httpContext);
}
}

public static class MyCustomMiddlewareExtensions


{
public static IApplicationBuilder UseMyCustomMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyCustomMiddleware>();
}
}

Lifetime and registration options contains a complete sample of middleware with scoped
lifetime services.

The following code is used to test the preceding middleware:

C#

using Middleware.Example;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMessageWriter, LoggingMessageWriter>();

var app = builder.Build();


app.UseHttpsRedirection();

app.UseMyCustomMiddleware();

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

app.Run();

The IMessageWriter interface and implementation:

C#

namespace Middleware.Example;

public interface IMessageWriter


{
void Write(string message);
}

public class LoggingMessageWriter : IMessageWriter


{

private readonly ILogger<LoggingMessageWriter> _logger;

public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>


_logger = logger;

public void Write(string message) =>


_logger.LogInformation(message);
}

Additional resources
Sample code used in this article
UseExtensions source on GitHub
Lifetime and registration options contains a complete sample of middleware with
scoped, transient, and singleton lifetime services.
DEEP DIVE: HOW IS THE ASP.NET CORE MIDDLEWARE PIPELINE BUILT
ASP.NET Core Middleware
Test ASP.NET Core middleware
Migrate HTTP handlers and modules to ASP.NET Core middleware
App startup in ASP.NET Core
Request Features in ASP.NET Core
Factory-based middleware activation in ASP.NET Core
Middleware activation with a third-party container in ASP.NET Core
Request and response operations in
ASP.NET Core
Article • 02/27/2023

By Justin Kotalik

This article explains how to read from the request body and write to the response body.
Code for these operations might be required when writing middleware. Outside of
writing middleware, custom code isn't generally required because the operations are
handled by MVC and Razor Pages.

There are two abstractions for the request and response bodies: Stream and Pipe. For
request reading, HttpRequest.Body is a Stream, and HttpRequest.BodyReader is a
PipeReader. For response writing, HttpResponse.Body is a Stream, and
HttpResponse.BodyWriter is a PipeWriter.

Pipelines are recommended over streams. Streams can be easier to use for some simple
operations, but pipelines have a performance advantage and are easier to use in most
scenarios. ASP.NET Core is starting to use pipelines instead of streams internally.
Examples include:

FormReader
TextReader

TextWriter
HttpResponse.WriteAsync

Streams aren't being removed from the framework. Streams continue to be used
throughout .NET, and many stream types don't have pipe equivalents, such as
FileStreams and ResponseCompression .

Stream examples
Suppose the goal is to create a middleware that reads the entire request body as a list
of strings, splitting on new lines. A simple stream implementation might look like the
following example:

2 Warning

The following code:


Is used to demonstrate the problems with not using a pipe to read the
request body.
Is not intended to be used in production apps.

C#

private async Task<List<string>> GetListOfStringsFromStream(Stream


requestBody)
{
// Build up the request body in a string builder.
StringBuilder builder = new StringBuilder();

// Rent a shared buffer to write the request body into.


byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

while (true)
{
var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0,
buffer.Length);
if (bytesRemaining == 0)
{
break;
}

// Append the encoded string into the string builder.


var encodedString = Encoding.UTF8.GetString(buffer, 0,
bytesRemaining);
builder.Append(encodedString);
}

ArrayPool<byte>.Shared.Return(buffer);

var entireRequestBody = builder.ToString();

// Split on \n in the string.


return new List<string>(entireRequestBody.Split("\n"));
}

If you would like to see code comments translated to languages other than English, let
us know in this GitHub discussion issue .

This code works, but there are some issues:

Before appending to the StringBuilder , the example creates another string


( encodedString ) that is thrown away immediately. This process occurs for all bytes
in the stream, so the result is extra memory allocation the size of the entire request
body.
The example reads the entire string before splitting on new lines. It's more efficient
to check for new lines in the byte array.

Here's an example that fixes some of the preceding issues:

2 Warning

The following code:

Is used to demonstrate the solutions to some problems in the preceding code


while not solving all the problems.
Is not intended to be used in production apps.

C#

private async Task<List<string>>


GetListOfStringsFromStreamMoreEfficient(Stream requestBody)
{
StringBuilder builder = new StringBuilder();
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
List<string> results = new List<string>();

while (true)
{
var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0,
buffer.Length);

if (bytesRemaining == 0)
{
results.Add(builder.ToString());
break;
}

// Instead of adding the entire buffer into the StringBuilder


// only add the remainder after the last \n in the array.
var prevIndex = 0;
int index;
while (true)
{
index = Array.IndexOf(buffer, (byte)'\n', prevIndex);
if (index == -1)
{
break;
}

var encodedString = Encoding.UTF8.GetString(buffer, prevIndex,


index - prevIndex);

if (builder.Length > 0)
{
// If there was a remainder in the string buffer, include it
in the next string.
results.Add(builder.Append(encodedString).ToString());
builder.Clear();
}
else
{
results.Add(encodedString);
}

// Skip past last \n


prevIndex = index + 1;
}

var remainingString = Encoding.UTF8.GetString(buffer, prevIndex,


bytesRemaining - prevIndex);
builder.Append(remainingString);
}

ArrayPool<byte>.Shared.Return(buffer);

return results;
}

This preceding example:

Doesn't buffer the entire request body in a StringBuilder unless there aren't any
newline characters.
Doesn't call Split on the string.

However, there are still a few issues:

If newline characters are sparse, much of the request body is buffered in the string.
The code continues to create strings ( remainingString ) and adds them to the
string buffer, which results in an extra allocation.

These issues are fixable, but the code is becoming progressively more complicated with
little improvement. Pipelines provide a way to solve these problems with minimal code
complexity.

Pipelines
The following example shows how the same scenario can be handled using a
PipeReader:

C#
private async Task<List<string>> GetListOfStringFromPipe(PipeReader reader)
{
List<string> results = new List<string>();

while (true)
{
ReadResult readResult = await reader.ReadAsync();
var buffer = readResult.Buffer;

SequencePosition? position = null;

do
{
// Look for a EOL in the buffer
position = buffer.PositionOf((byte)'\n');

if (position != null)
{
var readOnlySequence = buffer.Slice(0, position.Value);
AddStringToList(results, in readOnlySequence);

// Skip the line + the \n character (basically position)


buffer = buffer.Slice(buffer.GetPosition(1,
position.Value));
}
}
while (position != null);

if (readResult.IsCompleted && buffer.Length > 0)


{
AddStringToList(results, in buffer);
}

reader.AdvanceTo(buffer.Start, buffer.End);

// At this point, buffer will be updated to point one byte after the
last
// \n character.
if (readResult.IsCompleted)
{
break;
}
}

return results;
}

private static void AddStringToList(List<string> results, in


ReadOnlySequence<byte> readOnlySequence)
{
// Separate method because Span/ReadOnlySpan cannot be used in async
methods
ReadOnlySpan<byte> span = readOnlySequence.IsSingleSegment ?
readOnlySequence.First.Span : readOnlySequence.ToArray().AsSpan();
results.Add(Encoding.UTF8.GetString(span));
}

This example fixes many issues that the streams implementations had:

There's no need for a string buffer because the PipeReader handles bytes that
haven't been used.
Encoded strings are directly added to the list of returned strings.
Other than the ToArray call, and the memory used by the string, string creation is
allocation free.

Adapters
The Body , BodyReader , and BodyWriter properties are available for HttpRequest and
HttpResponse . When you set Body to a different stream, a new set of adapters
automatically adapt each type to the other. If you set HttpRequest.Body to a new stream,
HttpRequest.BodyReader is automatically set to a new PipeReader that wraps
HttpRequest.Body .

StartAsync
HttpResponse.StartAsync is used to indicate that headers are unmodifiable and to run

OnStarting callbacks. When using Kestrel as a server, calling StartAsync before using

the PipeReader guarantees that memory returned by GetMemory belongs to Kestrel's


internal Pipe rather than an external buffer.

Additional resources
System.IO.Pipelines in .NET
Write custom ASP.NET Core middleware
Request decompression in ASP.NET Core
Article • 08/19/2022

By David Acker

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.

When the Content-Encoding header value on a request matches one of the available
decompression providers, the middleware:

Uses the matching provider to wrap the HttpRequest.Body in an appropriate


decompression stream.
Removes the Content-Encoding header, indicating that the request body is no
longer compressed.

Requests that don't include a Content-Encoding header are ignored by the request
decompression middleware.

Decompression:

Occurs when the body of the request is being read. That is, decompression occurs
at the endpoint on model binding. The request body is not decompressed eagerly.
When attempting to read the decompressed request body, if the compressed data
is invalid for the specified Content-Encoding , an exception is thrown.

If the middleware encounters a request with compressed content but is unable to


decompress it, the request is passed to the next delegate in the pipeline. For example, a
request with an unsupported Content-Encoding header value or multiple Content-
Encoding header values, is passed to the next delegate in the pipeline. For example,
Brotli can throw System.InvalidOperationException : Decoder ran into invalid data,
Deflate and GZip can throw System.IO.InvalidDataException : The archive entry was
compressed using an unsupported compression method.

Configuration
The following code shows how to enable request decompression for the default
Content-Encoding types:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRequestDecompression();

var app = builder.Build();

app.UseRequestDecompression();

app.MapPost("/", (HttpRequest request) => Results.Stream(request.Body));

app.Run();

Default decompression providers


The Content-Encoding header values that the request decompression middleware
supports by default are listed in the following table:

Content-Encoding header values Description

br Brotli compressed data format

deflate DEFLATE compressed data format

gzip Gzip file format

Custom decompression providers


Support for custom encodings can be added by creating custom decompression
provider classes that implement IDecompressionProvider:

C#

public class CustomDecompressionProvider : IDecompressionProvider


{
public Stream GetDecompressionStream(Stream stream)
{
// Perform custom decompression logic here
return stream;
}
}
Custom decompression providers are registered with RequestDecompressionOptions
along with their corresponding Content-Encoding header values:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRequestDecompression(options =>
{
options.DecompressionProviders.Add("custom", new
CustomDecompressionProvider());
});

var app = builder.Build();

app.UseRequestDecompression();

app.MapPost("/", (HttpRequest request) => Results.Stream(request.Body));

app.Run();

Request size limits


In order to guard against zip bombs or decompression bombs :

The maximum size of the decompressed request body is limited to the request
body size limit enforced by the endpoint or server.
If the number of bytes read from the decompressed request body stream exceeds
the limit, an InvalidOperationException is thrown to prevent additional bytes from
being read from the stream.

In order of precedence, the maximum request size for an endpoint is set by:

1. IRequestSizeLimitMetadata.MaxRequestBodySize, such as
RequestSizeLimitAttribute or DisableRequestSizeLimitAttribute for MVC endpoints.
2. The global server size limit
IHttpMaxRequestBodySizeFeature.MaxRequestBodySize. MaxRequestBodySize can
be overridden per request with
IHttpMaxRequestBodySizeFeature.MaxRequestBodySize, but defaults to the limit
configured for the web server implementation.

Web server implementation MaxRequestBodySize configuration

HTTP.sys HttpSysOptions.MaxRequestBodySize

IIS IISServerOptions.MaxRequestBodySize
Web server implementation MaxRequestBodySize configuration

Kestrel KestrelServerLimits.MaxRequestBodySize

2 Warning

Disabling the request body size limit poses a security risk in regards to uncontrolled
resource consumption, particularly if the request body is being buffered. Ensure
that safeguards are in place to mitigate the risk of denial-of-service (DoS)
attacks.

Additional Resources
ASP.NET Core Middleware
Mozilla Developer Network: Content-Encoding
Brotli Compressed Data Format
DEFLATE Compressed Data Format Specification version 1.3
GZIP file format specification version 4.3
Factory-based middleware activation in
ASP.NET Core
Article • 06/03/2022

IMiddlewareFactory/IMiddleware is an extensibility point for middleware activation that


offers the following benefits:

Activation per client request (injection of scoped services)


Strong typing of middleware

UseMiddleware extension methods check if a middleware's registered type implements


IMiddleware. If it does, the IMiddlewareFactory instance registered in the container is
used to resolve the IMiddleware implementation instead of using the convention-based
middleware activation logic. The middleware is registered as a scoped or transient
service in the app's service container.

IMiddleware is activated per client request (connection), so scoped services can be


injected into the middleware's constructor.

IMiddleware
IMiddleware defines middleware for the app's request pipeline. The
InvokeAsync(HttpContext, RequestDelegate) method handles requests and returns a
Task that represents the execution of the middleware.

Middleware activated by convention:

C#

public class ConventionalMiddleware


{
private readonly RequestDelegate _next;

public ConventionalMiddleware(RequestDelegate next)


=> _next = next;

public async Task InvokeAsync(HttpContext context, SampleDbContext


dbContext)
{
var keyValue = context.Request.Query["key"];

if (!string.IsNullOrWhiteSpace(keyValue))
{
dbContext.Requests.Add(new Request("Conventional", keyValue));
await dbContext.SaveChangesAsync();
}

await _next(context);
}
}

Middleware activated by MiddlewareFactory:

C#

public class FactoryActivatedMiddleware : IMiddleware


{
private readonly SampleDbContext _dbContext;

public FactoryActivatedMiddleware(SampleDbContext dbContext)


=> _dbContext = dbContext;

public async Task InvokeAsync(HttpContext context, RequestDelegate next)


{
var keyValue = context.Request.Query["key"];

if (!string.IsNullOrWhiteSpace(keyValue))
{
_dbContext.Requests.Add(new Request("Factory", keyValue));

await _dbContext.SaveChangesAsync();
}

await next(context);
}
}

Extensions are created for the middleware:

C#

public static class MiddlewareExtensions


{
public static IApplicationBuilder UseConventionalMiddleware(
this IApplicationBuilder app)
=> app.UseMiddleware<ConventionalMiddleware>();

public static IApplicationBuilder UseFactoryActivatedMiddleware(


this IApplicationBuilder app)
=> app.UseMiddleware<FactoryActivatedMiddleware>();
}

It isn't possible to pass objects to the factory-activated middleware with UseMiddleware:


C#

public static IApplicationBuilder UseFactoryActivatedMiddleware(


this IApplicationBuilder app, bool option)
{
// Passing 'option' as an argument throws a NotSupportedException at
runtime.
return app.UseMiddleware<FactoryActivatedMiddleware>(option);
}

The factory-activated middleware is added to the built-in container in Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<SampleDbContext>
(options => options.UseInMemoryDatabase("SampleDb"));

builder.Services.AddTransient<FactoryActivatedMiddleware>();

Both middleware are registered in the request processing pipeline, also in Program.cs :

C#

var app = builder.Build();

app.UseConventionalMiddleware();
app.UseFactoryActivatedMiddleware();

IMiddlewareFactory
IMiddlewareFactory provides methods to create middleware. The middleware factory
implementation is registered in the container as a scoped service.

The default IMiddlewareFactory implementation, MiddlewareFactory, is found in the


Microsoft.AspNetCore.Http package.

Additional resources
View or download sample code (how to download)
ASP.NET Core Middleware
Middleware activation with a third-party container in ASP.NET Core
Middleware activation with a third-party
container in ASP.NET Core
Article • 06/03/2022

This article demonstrates how to use IMiddlewareFactory and IMiddleware as an


extensibility point for middleware activation with a third-party container. For
introductory information on IMiddlewareFactory and IMiddleware , see Factory-based
middleware activation in ASP.NET Core.

View or download sample code (how to download)

The sample app demonstrates middleware activation by an IMiddlewareFactory


implementation, SimpleInjectorMiddlewareFactory . The sample uses the Simple
Injector dependency injection (DI) container.

The sample's middleware implementation records the value provided by a query string
parameter ( key ). The middleware uses an injected database context (a scoped service)
to record the query string value in an in-memory database.

7 Note

The sample app uses Simple Injector purely for demonstration purposes. Use of
Simple Injector isn't an endorsement. Middleware activation approaches described
in the Simple Injector documentation and GitHub issues are recommended by the
maintainers of Simple Injector. For more information, see the Simple Injector
documentation and Simple Injector GitHub repository .

IMiddlewareFactory
IMiddlewareFactory provides methods to create middleware.

In the sample app, a middleware factory is implemented to create a


SimpleInjectorActivatedMiddleware instance. The middleware factory uses the Simple
Injector container to resolve the middleware:

C#

public class SimpleInjectorMiddlewareFactory : IMiddlewareFactory


{
private readonly Container _container;
public SimpleInjectorMiddlewareFactory(Container container)
{
_container = container;
}

public IMiddleware Create(Type middlewareType)


{
return _container.GetInstance(middlewareType) as IMiddleware;
}

public void Release(IMiddleware middleware)


{
// The container is responsible for releasing resources.
}
}

IMiddleware
IMiddleware defines middleware for the app's request pipeline.

Middleware activated by an IMiddlewareFactory implementation


( Middleware/SimpleInjectorActivatedMiddleware.cs ):

C#

public class SimpleInjectorActivatedMiddleware : IMiddleware


{
private readonly AppDbContext _db;

public SimpleInjectorActivatedMiddleware(AppDbContext db)


{
_db = db;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)


{
var keyValue = context.Request.Query["key"];

if (!string.IsNullOrWhiteSpace(keyValue))
{
_db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation =
"SimpleInjectorActivatedMiddleware",
Value = keyValue
});

await _db.SaveChangesAsync();
}
await next(context);
}
}

An extension is created for the middleware ( Middleware/MiddlewareExtensions.cs ):

C#

public static class MiddlewareExtensions


{
public static IApplicationBuilder UseSimpleInjectorActivatedMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<SimpleInjectorActivatedMiddleware>();
}
}

Startup.ConfigureServices must perform several tasks:

Set up the Simple Injector container.


Register the factory and middleware.
Make the app's database context available from the Simple Injector container.

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddRazorPages();

// Replace the default middleware factory with the


// SimpleInjectorMiddlewareFactory.
services.AddTransient<IMiddlewareFactory>(_ =>
{
return new SimpleInjectorMiddlewareFactory(_container);
});

// Wrap ASP.NET Core requests in a Simple Injector execution


// context.
services.UseSimpleInjectorAspNetRequestScoping(_container);

// Provide the database context from the Simple


// Injector container whenever it's requested from
// the default service container.
services.AddScoped<AppDbContext>(provider =>
_container.GetInstance<AppDbContext>());

_container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

_container.Register<AppDbContext>(() =>
{
var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
optionsBuilder.UseInMemoryDatabase("InMemoryDb");
return new AppDbContext(optionsBuilder.Options);
}, Lifestyle.Scoped);

_container.Register<SimpleInjectorActivatedMiddleware>();

_container.Verify();
}

The middleware is registered in the request processing pipeline in Startup.Configure :

C#

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}

app.UseSimpleInjectorActivatedMiddleware();

app.UseStaticFiles();
app.UseRouting();

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

Additional resources
Middleware
Factory-based middleware activation
Simple Injector GitHub repository
Simple Injector documentation
WebApplication and
WebApplicationBuilder in Minimal API
apps
Article • 04/19/2023

WebApplication
The following code is generated by an ASP.NET Core template:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

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

app.Run();

The preceding code can be created via dotnet new web on the command line or
selecting the Empty Web template in Visual Studio.

The following code creates a WebApplication ( app ) without explicitly creating a


WebApplicationBuilder:

C#

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create initializes a new instance of the WebApplication class with


preconfigured defaults.

WebApplication automatically adds the following middleware depending on certain


conditions:

UseDeveloperExceptionPage is added first when the HostingEnvironment is


"Development" .
UseRouting is added second if user code didn't already call UseRouting and if there
are endpoints configured, for example app.MapGet .
UseEndpoints is added at the end of the middleware pipeline if any endpoints are
configured.
UseAuthentication is added immediately after UseRouting if
IAuthenticationSchemeProvider can be detected in the service provider.
IAuthenticationSchemeProvider is added by default when using
AddAuthentication, and services are detected using IServiceProviderIsService.
UseAuthorization is added next if IAuthorizationHandlerProvider can be detected
in the service provider. IAuthorizationHandlerProvider is added by default when
using AddAuthorization, and services are detected using
IServiceProviderIsService .
User configured middleware and endpoints are added between UseRouting and
UseEndpoints .

The following code is effectively what the automatic middleware being added to the app
produces:

C#

if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

In some cases, the default middleware configuration isn't correct for the app and
requires modification. For example, UseCors should be called before UseAuthentication
and UseAuthorization. The app needs to call UseAuthentication and UseAuthorization if
UseCors is called:

C#

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

If middleware should be run before route matching occurs, UseRouting should be called
and the middleware should be placed before the call to UseRouting . UseEndpoints isn't
required in this case as it is automatically added as described previously:

C#

app.Use((context, next) =>


{
return next(context);
});

app.UseRouting();

// other middleware and endpoints

When adding a terminal middleware:

The middleware must be added after UseEndpoints .


The app needs to call UseRouting and UseEndpoints so that the terminal
middleware can be placed at the correct location.

C#

app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});

Terminal middleware is middleware that runs if no endpoint handles the request.


Working with ports
When a web app is created with Visual Studio or dotnet new , a
Properties/launchSettings.json file is created that specifies the ports the app responds

to. In the port setting samples that follow, running the app from Visual Studio returns an
error dialog Unable to connect to web server 'AppName' . Visual Studio returns an error
because it's expecting the port specified in Properties/launchSettings.json , but the
app is using the port specified by app.Run("http://localhost:3000") . Run the following
port changing samples from the command line.

The following sections set the port the app responds to.

C#

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

In the preceding code, the app responds to port 3000 .

Multiple ports

In the following code, the app responds to port 3000 and 4000 .

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

Set the port from the command line

The following command makes the app respond to port 7777 :

.NET CLI

dotnet run --urls="https://localhost:7777"


If the Kestrel endpoint is also configured in the appsettings.json file, the
appsettings.json file specified URL is used. For more information, see Kestrel endpoint
configuration

Read the port from environment


The following code reads the port from the environment:

C#

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

The preferred way to set the port from the environment is to use the ASPNETCORE_URLS
environment variable, which is shown in the following section.

Set the ports via the ASPNETCORE_URLS environment variable


The ASPNETCORE_URLS environment variable is available to set the port:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS supports multiple URLs:

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

Listen on all interfaces


The following samples demonstrate listening on all interfaces

http://*:3000
C#

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

Listen on all interfaces using ASPNETCORE_URLS


The preceding samples can use ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Specify HTTPS with development certificate


C#

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

For more information on the development certificate, see Trust the ASP.NET Core HTTPS
development certificate on Windows and macOS.

Specify HTTPS using a custom certificate


The following sections show how to specify the custom certificate using the
appsettings.json file and via configuration.

Specify the custom certificate with appsettings.json

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}

Specify the custom certificate via configuration

C#

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key


builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Use the certificate APIs

C#

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath,
"cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath,
"key.pem");

httpsOptions.ServerCertificate =
X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Read the environment


C#

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}

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


app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

For more information using the environment, see Use multiple environments in ASP.NET
Core

Configuration
The following code reads from the configuration system:

C#

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Hello";

app.MapGet("/", () => message);

app.Run();

For more information, see Configuration in ASP.NET Core

Logging
The following code writes a message to the log on application startup:

C#

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

For more information, see Logging in .NET Core and ASP.NET Core

Access the Dependency Injection (DI) container


The following code shows how to get services from the DI container during application
startup:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())


{
var sampleService =
scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}

app.Run();

For more information, see Dependency injection in ASP.NET Core.

WebApplicationBuilder
This section contains sample code using WebApplicationBuilder.

Change the content root, application name, and


environment
The following code sets the content root, application name, and environment:

C#

var builder = WebApplication.CreateBuilder(new WebApplicationOptions


{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name:
{builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name:
{builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path:
{builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder


class with preconfigured defaults.

For more information, see ASP.NET Core fundamentals overview

Change the content root, app name, and environment by


environment variables or command line
The following table shows the environment variable and command-line argument used
to change the content root, app name, and environment:

feature Environment variable Command-line argument

Application name ASPNETCORE_APPLICATIONNAME --applicationName

Environment name ASPNETCORE_ENVIRONMENT --environment

Content root ASPNETCORE_CONTENTROOT --contentRoot

Add configuration providers


The following sample adds the INI configuration provider:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

For detailed information, see File configuration providers in Configuration in ASP.NET


Core.

Read configuration
By default the WebApplicationBuilder reads configuration from multiple sources,
including:

appSettings.json and appSettings.{environment}.json

Environment variables
The command line

For a complete list of configuration sources read, see Default configuration in


Configuration in ASP.NET Core

C#

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Read the environment


The following code reads HelloKey from configuration and displays the value at the /
endpoint. If the configuration value is null, "Hello" is assigned to message :

C#

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}

var app = builder.Build();

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

app.Run();

Add logging providers


C#
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.


builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

Add services
C#

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.


builder.Services.AddMemoryCache();

// Add a custom scoped service.


builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Customize the IHostBuilder


Existing extension methods on IHostBuilder can be accessed using the Host property:

C#

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.


builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout =
TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

Customize the IWebHostBuilder


Extension methods on IWebHostBuilder can be accessed using the
WebApplicationBuilder.WebHost property.

C#

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based


builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Change the web root


By default, the web root is relative to the content root in the wwwroot folder. Web root is
where the static files middleware looks for static files. Web root can be changed with
WebHostOptions , the command line, or with the UseWebRoot method:

C#

var builder = WebApplication.CreateBuilder(new WebApplicationOptions


{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

Custom dependency injection (DI) container


The following example uses Autofac :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't


// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Add Middleware
Any existing ASP.NET Core middleware can be configured on the WebApplication :

C#

var app = WebApplication.Create(args);

// Setup the file server to serve static files.


app.UseFileServer();

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

app.Run();

For more information, see ASP.NET Core Middleware

Developer exception page


WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder
class with preconfigured defaults. The developer exception page is enabled in the
preconfigured defaults. When the following code is run in the development
environment, navigating to / renders a friendly page that shows the exception.

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an
exception.");
});

app.Run();
.NET Generic Host in ASP.NET Core
Article • 04/11/2023

This article provides information on using the .NET Generic Host in ASP.NET Core.

The ASP.NET Core templates create a WebApplicationBuilder and WebApplication, which


provide a streamlined way to configure and run web applications without a Startup
class. For more information on WebApplicationBuilder and WebApplication , see Migrate
from ASP.NET Core 5.0 to 6.0.

For information on using the .NET Generic Host in console apps, see .NET Generic Host.

Host definition
A host is an object that encapsulates an app's resources, such as:

Dependency injection (DI)


Logging
Configuration
IHostedService implementations

When a host starts, it calls IHostedService.StartAsync on each implementation of


IHostedService registered in the service container's collection of hosted services. In a
web app, one of the IHostedService implementations is a web service that starts an
HTTP server implementation.

Including all of the app's interdependent resources in one object enables control over
app startup and graceful shutdown.

Set up a host
The host is typically configured, built, and run by code in the Program.cs . The following
code creates a host with an IHostedService implementation added to the DI container:

C#

await Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<SampleHostedService>();
})
.Build()
.RunAsync();
For an HTTP workload, call ConfigureWebHostDefaults after CreateDefaultBuilder:

C#

await Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build()
.RunAsync();

Default builder settings


The CreateDefaultBuilder method:

Sets the content root to the path returned by GetCurrentDirectory.


Loads host configuration from:
Environment variables prefixed with DOTNET_ .
Command-line arguments.
Loads app configuration from:
appsettings.json .

appsettings.{Environment}.json .

User secrets when the app runs in the Development environment.


Environment variables.
Command-line arguments.
Adds the following logging providers:
Console
Debug
EventSource
EventLog (only when running on Windows)
Enables scope validation and dependency validation when the environment is
Development.

The ConfigureWebHostDefaults method:

Loads host configuration from environment variables prefixed with ASPNETCORE_ .


Sets Kestrel server as the web server and configures it using the app's hosting
configuration providers. For the Kestrel server's default options, see Configure
options for the ASP.NET Core Kestrel web server.
Adds Host Filtering middleware.
Adds Forwarded Headers middleware if ASPNETCORE_FORWARDEDHEADERS_ENABLED
equals true .
Enables IIS integration. For the IIS default options, see Host ASP.NET Core on
Windows with IIS.

The Settings for all app types and Settings for web apps sections later in this article
show how to override default builder settings.

Framework-provided services
The following services are registered automatically:

IHostApplicationLifetime
IHostLifetime
IHostEnvironment / IWebHostEnvironment

For more information on framework-provided services, see Dependency injection in


ASP.NET Core.

IHostApplicationLifetime
Inject the IHostApplicationLifetime (formerly IApplicationLifetime ) service into any
class to handle post-startup and graceful shutdown tasks. Three properties on the
interface are cancellation tokens used to register app start and app stop event handler
methods. The interface also includes a StopApplication method, which allows apps to
request a graceful shutdown.

When performing a graceful shutdown, the host:

Triggers the ApplicationStopping event handlers, which allows the app to run logic
before the shutdown process begins.
Stops the server, which disables new connections. The server waits for requests on
existing connections to complete, for as long as the shutdown timeout allows. The
server sends the connection close header for further requests on existing
connections.
Triggers the ApplicationStopped event handlers, which allows the app to run logic
after the application has shutdown.

The following example is an IHostedService implementation that registers


IHostApplicationLifetime event handlers:

C#
public class HostApplicationLifetimeEventsHostedService : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;

public HostApplicationLifetimeEventsHostedService(
IHostApplicationLifetime hostApplicationLifetime)
=> _hostApplicationLifetime = hostApplicationLifetime;

public Task StartAsync(CancellationToken cancellationToken)


{
_hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
_hostApplicationLifetime.ApplicationStopping.Register(OnStopping);
_hostApplicationLifetime.ApplicationStopped.Register(OnStopped);

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)


=> Task.CompletedTask;

private void OnStarted()


{
// ...
}

private void OnStopping()


{
// ...
}

private void OnStopped()


{
// ...
}
}

IHostLifetime
The IHostLifetime implementation controls when the host starts and when it stops. The
last implementation registered is used.

Microsoft.Extensions.Hosting.Internal.ConsoleLifetime is the default IHostLifetime


implementation. ConsoleLifetime :

Listens for Ctrl + C /SIGINT (Windows), ⌘ + C (macOS), or SIGTERM and calls


StopApplication to start the shutdown process.
Unblocks extensions such as RunAsync and WaitForShutdownAsync.
IHostEnvironment
Inject the IHostEnvironment service into a class to get information about the following
settings:

ApplicationName
EnvironmentName
ContentRootPath

Web apps implement the IWebHostEnvironment interface, which inherits


IHostEnvironment and adds the WebRootPath.

Host configuration
Host configuration is used for the properties of the IHostEnvironment implementation.

Host configuration is available from HostBuilderContext.Configuration inside


ConfigureAppConfiguration. After ConfigureAppConfiguration ,
HostBuilderContext.Configuration is replaced with the app config.

To add host configuration, call ConfigureHostConfiguration on IHostBuilder .


ConfigureHostConfiguration can be called multiple times with additive results. The host

uses whichever option sets a value last on a given key.

The environment variable provider with prefix DOTNET_ and command-line arguments
are included by CreateDefaultBuilder . For web apps, the environment variable provider
with prefix ASPNETCORE_ is added. The prefix is removed when the environment variables
are read. For example, the environment variable value for ASPNETCORE_ENVIRONMENT
becomes the host configuration value for the environment key.

The following example creates host configuration:

C#

Host.CreateDefaultBuilder(args)
.ConfigureHostConfiguration(hostConfig =>
{
hostConfig.SetBasePath(Directory.GetCurrentDirectory());
hostConfig.AddJsonFile("hostsettings.json", optional: true);
hostConfig.AddEnvironmentVariables(prefix: "PREFIX_");
hostConfig.AddCommandLine(args);
});
App configuration
App configuration is created by calling ConfigureAppConfiguration on IHostBuilder .
ConfigureAppConfiguration can be called multiple times with additive results. The app

uses whichever option sets a value last on a given key.

The configuration created by ConfigureAppConfiguration is available at


HostBuilderContext.Configuration for subsequent operations and as a service from DI.
The host configuration is also added to the app configuration.

For more information, see Configuration in ASP.NET Core.

Settings for all app types


This section lists host settings that apply to both HTTP and non-HTTP workloads. By
default, environment variables used to configure these settings can have a DOTNET_ or
ASPNETCORE_ prefix, which appear in the following list of settings as the {PREFIX_}

placeholder. For more information, see the Default builder settings section and
Configuration: Environment variables.

ApplicationName
The IHostEnvironment.ApplicationName property is set from host configuration during
host construction.

Key: applicationName
Type: string
Default: The name of the assembly that contains the app's entry point.
Environment variable: {PREFIX_}APPLICATIONNAME

To set this value, use the environment variable.

ContentRoot
The IHostEnvironment.ContentRootPath property determines where the host begins
searching for content files. If the path doesn't exist, the host fails to start.

Key: contentRoot
Type: string
Default: The folder where the app assembly resides.
Environment variable: {PREFIX_}CONTENTROOT
To set this value, use the environment variable or call UseContentRoot on IHostBuilder :

C#

Host.CreateDefaultBuilder(args)
.UseContentRoot("/path/to/content/root")
// ...

For more information, see:

Fundamentals: Content root


WebRoot

EnvironmentName
The IHostEnvironment.EnvironmentName property can be set to any value. Framework-
defined values include Development , Staging , and Production . Values aren't case-
sensitive.

Key: environment
Type: string
Default: Production
Environment variable: {PREFIX_}ENVIRONMENT

To set this value, use the environment variable or call UseEnvironment on IHostBuilder :

C#

Host.CreateDefaultBuilder(args)
.UseEnvironment("Development")
// ...

ShutdownTimeout
HostOptions.ShutdownTimeout sets the timeout for StopAsync. The default value is five
seconds. During the timeout period, the host:

Triggers IHostApplicationLifetime.ApplicationStopping.
Attempts to stop hosted services, logging errors for services that fail to stop.

If the timeout period expires before all of the hosted services stop, any remaining active
services are stopped when the app shuts down. The services stop even if they haven't
finished processing. If services require more time to stop, increase the timeout.
Key: shutdownTimeoutSeconds
Type: int
Default: 5 seconds
Environment variable: {PREFIX_}SHUTDOWNTIMEOUTSECONDS

To set this value, use the environment variable or configure HostOptions . The following
example sets the timeout to 20 seconds:

C#

Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(20);
});
});

Disable app configuration reload on change


By default, appsettings.json and appsettings.{Environment}.json are reloaded when
the file changes. To disable this reload behavior in ASP.NET Core 5.0 or later, set the
hostBuilder:reloadConfigOnChange key to false .

Key: hostBuilder:reloadConfigOnChange
Type: bool ( true or false )
Default: true
Command-line argument: hostBuilder:reloadConfigOnChange
Environment variable: {PREFIX_}hostBuilder:reloadConfigOnChange

2 Warning

The colon ( : ) separator doesn't work with environment variable hierarchical keys
on all platforms. For more information, see Environment variables.

Settings for web apps


Some host settings apply only to HTTP workloads. By default, environment variables
used to configure these settings can have a DOTNET_ or ASPNETCORE_ prefix, which
appear in the following list of settings as the {PREFIX_} placeholder.
Extension methods on IWebHostBuilder are available for these settings. Code samples
that show how to call the extension methods assume webBuilder is an instance of
IWebHostBuilder , as in the following example:

C#

Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// ...
});

CaptureStartupErrors
When false , errors during startup result in the host exiting. When true , the host
captures exceptions during startup and attempts to start the server.

Key: captureStartupErrors
Type: bool ( true / 1 or false / 0 )
Default: Defaults to false unless the app runs with Kestrel behind IIS, where the default
is true .
Environment variable: {PREFIX_}CAPTURESTARTUPERRORS

To set this value, use configuration or call CaptureStartupErrors :

C#

webBuilder.CaptureStartupErrors(true);

DetailedErrors
When enabled, or when the environment is Development , the app captures detailed
errors.

Key: detailedErrors
Type: bool ( true / 1 or false / 0 )
Default: false
Environment variable: {PREFIX_}DETAILEDERRORS

To set this value, use configuration or call UseSetting :

C#
webBuilder.UseSetting(WebHostDefaults.DetailedErrorsKey, "true");

HostingStartupAssemblies
A semicolon-delimited string of hosting startup assemblies to load on startup. Although
the configuration value defaults to an empty string, the hosting startup assemblies
always include the app's assembly. When hosting startup assemblies are provided,
they're added to the app's assembly for loading when the app builds its common
services during startup.

Key: hostingStartupAssemblies
Type: string
Default: Empty string
Environment variable: {PREFIX_}HOSTINGSTARTUPASSEMBLIES

To set this value, use configuration or call UseSetting :

C#

webBuilder.UseSetting(
WebHostDefaults.HostingStartupAssembliesKey, "assembly1;assembly2");

HostingStartupExcludeAssemblies
A semicolon-delimited string of hosting startup assemblies to exclude on startup.

Key: hostingStartupExcludeAssemblies
Type: string
Default: Empty string
Environment variable: {PREFIX_}HOSTINGSTARTUPEXCLUDEASSEMBLIES

To set this value, use configuration or call UseSetting :

C#

webBuilder.UseSetting(
WebHostDefaults.HostingStartupExcludeAssembliesKey,
"assembly1;assembly2");

HTTPS_Port
The HTTPS redirect port. Used in enforcing HTTPS.

Key: https_port
Type: string
Default: A default value isn't set.
Environment variable: {PREFIX_}HTTPS_PORT

To set this value, use configuration or call UseSetting :

C#

webBuilder.UseSetting("https_port", "8080");

PreferHostingUrls
Indicates whether the host should listen on the URLs configured with the
IWebHostBuilder instead of those URLs configured with the IServer implementation.

Key: preferHostingUrls
Type: bool ( true / 1 or false / 0 )
Default: true
Environment variable: {PREFIX_}PREFERHOSTINGURLS

To set this value, use the environment variable or call PreferHostingUrls :

C#

webBuilder.PreferHostingUrls(true);

PreventHostingStartup
Prevents the automatic loading of hosting startup assemblies, including hosting startup
assemblies configured by the app's assembly. For more information, see Use hosting
startup assemblies in ASP.NET Core.

Key: preventHostingStartup
Type: bool ( true / 1 or false / 0 )
Default: false
Environment variable: {PREFIX_}PREVENTHOSTINGSTARTUP

To set this value, use the environment variable or call UseSetting :


C#

webBuilder.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true");

StartupAssembly
The assembly to search for the Startup class.

Key: startupAssembly
Type: string
Default: The app's assembly
Environment variable: {PREFIX_}STARTUPASSEMBLY

To set this value, use the environment variable or call UseStartup . UseStartup can take
an assembly name ( string ) or a type ( TStartup ). If multiple UseStartup methods are
called, the last one takes precedence.

C#

webBuilder.UseStartup("StartupAssemblyName");

C#

webBuilder.UseStartup<Startup>();

SuppressStatusMessages
When enabled, suppresses hosting startup status messages.

Key: suppressStatusMessages
Type: bool ( true / 1 or false / 0 )
Default: false
Environment variable: {PREFIX_}SUPPRESSSTATUSMESSAGES

To set this value, use configuration or call UseSetting :

C#

webBuilder.UseSetting(WebHostDefaults.SuppressStatusMessagesKey, "true");
URLs
A semicolon-delimited list of IP addresses or host addresses with ports and protocols
that the server should listen on for requests. For example, http://localhost:123 . Use "*"
to indicate that the server should listen for requests on any IP address or hostname
using the specified port and protocol (for example, http://*:5000 ). The protocol
( http:// or https:// ) must be included with each URL. Supported formats vary among
servers.

Key: urls
Type: string
Default: http://localhost:5000 and https://localhost:5001
Environment variable: {PREFIX_}URLS

To set this value, use the environment variable or call UseUrls :

C#

webBuilder.UseUrls("http://*:5000;http://localhost:5001;https://hostname:500
2");

Kestrel has its own endpoint configuration API. For more information, see Configure
endpoints for the ASP.NET Core Kestrel web server.

WebRoot
The IWebHostEnvironment.WebRootPath property determines the relative path to the
app's static assets. If the path doesn't exist, a no-op file provider is used.

Key: webroot
Type: string
Default: The default is wwwroot . The path to {content root}/wwwroot must exist.
Environment variable: {PREFIX_}WEBROOT

To set this value, use the environment variable or call UseWebRoot on IWebHostBuilder :

C#

webBuilder.UseWebRoot("public");

For more information, see:


Fundamentals: Web root
ContentRoot

Manage the host lifetime


Call methods on the built IHost implementation to start and stop the app. These
methods affect all IHostedService implementations that are registered in the service
container.

Run
Run runs the app and blocks the calling thread until the host is shut down.

RunAsync
RunAsync runs the app and returns a Task that completes when the cancellation token
or shutdown is triggered.

RunConsoleAsync
RunConsoleAsync enables console support, builds and starts the host, and waits for
Ctrl + C /SIGINT (Windows), ⌘ + C (macOS), or SIGTERM to shut down.

Start
Start starts the host synchronously.

StartAsync
StartAsync starts the host and returns a Task that completes when the cancellation token
or shutdown is triggered.

WaitForStartAsync is called at the start of StartAsync , which waits until it's complete
before continuing. This method can be used to delay startup until signaled by an
external event.

StopAsync
StopAsync attempts to stop the host within the provided timeout.
WaitForShutdown
WaitForShutdown blocks the calling thread until shutdown is triggered by the
IHostLifetime, such as via Ctrl + C /SIGINT (Windows), ⌘ + C (macOS), or SIGTERM.

WaitForShutdownAsync
WaitForShutdownAsync returns a Task that completes when shutdown is triggered via
the given token and calls StopAsync.

Additional resources
Background tasks with hosted services in ASP.NET Core
GitHub link to Generic Host source

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .
ASP.NET Core Web Host
Article • 04/11/2023

ASP.NET Core apps configure and launch a host. The host is responsible for app startup
and lifetime management. At a minimum, the host configures a server and a request
processing pipeline. The host can also set up logging, dependency injection, and
configuration.

This article covers the Web Host, which remains available only for backward
compatibility. The ASP.NET Core templates create a WebApplicationBuilder and
WebApplication, which is recommended for web apps. For more information on
WebApplicationBuilder and WebApplication , see Migrate from ASP.NET Core 5.0 to 6.0

Set up a host
Create a host using an instance of IWebHostBuilder. This is typically performed in the
app's entry point, the Main method in Program.cs . A typical app calls
CreateDefaultBuilder to start setting up a host:

C#

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}

The code that calls CreateDefaultBuilder is in a method named CreateWebHostBuilder ,


which separates it from the code in Main that calls Run on the builder object. This
separation is required if you use Entity Framework Core tools. The tools expect to find a
CreateWebHostBuilder method that they can call at design time to configure the host

without running the app. An alternative is to implement IDesignTimeDbContextFactory .


For more information, see Design-time DbContext Creation.

CreateDefaultBuilder performs the following tasks:


Configures Kestrel server as the web server using the app's hosting configuration
providers. For the Kestrel server's default options, see Configure options for the
ASP.NET Core Kestrel web server.
Sets the content root to the path returned by Directory.GetCurrentDirectory.
Loads host configuration from:
Environment variables prefixed with ASPNETCORE_ (for example,
ASPNETCORE_ENVIRONMENT ).
Command-line arguments.
Loads app configuration in the following order from:
appsettings.json .

appsettings.{Environment}.json .

User secrets when the app runs in the Development environment using the entry
assembly.
Environment variables.
Command-line arguments.
Configures logging for console and debug output. Logging includes log filtering
rules specified in a Logging configuration section of an appsettings.json or
appsettings.{Environment}.json file.

When running behind IIS with the ASP.NET Core Module, CreateDefaultBuilder
enables IIS Integration, which configures the app's base address and port. IIS
Integration also configures the app to capture startup errors. For the IIS default
options, see Host ASP.NET Core on Windows with IIS.
Sets ServiceProviderOptions.ValidateScopes to true if the app's environment is
Development. For more information, see Scope validation.

The configuration defined by CreateDefaultBuilder can be overridden and augmented


by ConfigureAppConfiguration, ConfigureLogging, and other methods and extension
methods of IWebHostBuilder. A few examples follow:

ConfigureAppConfiguration is used to specify additional IConfiguration for the


app. The following ConfigureAppConfiguration call adds a delegate to include app
configuration in the appsettings.xml file. ConfigureAppConfiguration may be
called multiple times. Note that this configuration doesn't apply to the host (for
example, server URLs or environment). See the Host configuration values section.

C#

WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddXmlFile("appsettings.xml", optional: true,
reloadOnChange: true);
})
...

The following ConfigureLogging call adds a delegate to configure the minimum


logging level (SetMinimumLevel) to LogLevel.Warning. This setting overrides the
settings in appsettings.Development.json ( LogLevel.Debug ) and
appsettings.Production.json ( LogLevel.Error ) configured by
CreateDefaultBuilder . ConfigureLogging may be called multiple times.

C#

WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Warning);
})
...

The following call to ConfigureKestrel overrides the default


Limits.MaxRequestBodySize of 30,000,000 bytes established when Kestrel was
configured by CreateDefaultBuilder :

C#

WebHost.CreateDefaultBuilder(args)
.ConfigureKestrel((context, options) =>
{
options.Limits.MaxRequestBodySize = 20000000;
});

The content root determines where the host searches for content files, such as MVC
view files. When the app is started from the project's root folder, the project's root
folder is used as the content root. This is the default used in Visual Studio and the
dotnet new templates.

For more information on app configuration, see Configuration in ASP.NET Core.

7 Note

As an alternative to using the static CreateDefaultBuilder method, creating a host


from WebHostBuilder is a supported approach with ASP.NET Core 2.x.
When setting up a host, Configure and ConfigureServices methods can be provided. If a
Startup class is specified, it must define a Configure method. For more information, see
App startup in ASP.NET Core. Multiple calls to ConfigureServices append to one
another. Multiple calls to Configure or UseStartup on the WebHostBuilder replace
previous settings.

Host configuration values


WebHostBuilder relies on the following approaches to set the host configuration values:

Host builder configuration, which includes environment variables with the format
ASPNETCORE_{configurationKey} . For example, ASPNETCORE_ENVIRONMENT .
Extensions such as UseContentRoot and UseConfiguration (see the Override
configuration section).
UseSetting and the associated key. When setting a value with UseSetting , the
value is set as a string regardless of the type.

The host uses whichever option sets a value last. For more information, see Override
configuration in the next section.

Application Key (Name)


The IWebHostEnvironment.ApplicationName property is automatically set when
UseStartup or Configure is called during host construction. The value is set to the name
of the assembly containing the app's entry point. To set the value explicitly, use the
WebHostDefaults.ApplicationKey:

Key: applicationName
Type: string
Default: The name of the assembly containing the app's entry point.
Set using: UseSetting
Environment variable: ASPNETCORE_APPLICATIONNAME

C#

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.ApplicationKey, "CustomApplicationName")

Capture Startup Errors


This setting controls the capture of startup errors.
Key: captureStartupErrors
Type: bool ( true or 1 )
Default: Defaults to false unless the app runs with Kestrel behind IIS, where the default
is true .
Set using: CaptureStartupErrors
Environment variable: ASPNETCORE_CAPTURESTARTUPERRORS

When false , errors during startup result in the host exiting. When true , the host
captures exceptions during startup and attempts to start the server.

C#

WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)

Content root
This setting determines where ASP.NET Core begins searching for content files.

Key: contentRoot
Type: string
Default: Defaults to the folder where the app assembly resides.
Set using: UseContentRoot
Environment variable: ASPNETCORE_CONTENTROOT

The content root is also used as the base path for the web root. If the content root path
doesn't exist, the host fails to start.

C#

WebHost.CreateDefaultBuilder(args)
.UseContentRoot("c:\\<content-root>")

For more information, see:

Fundamentals: Content root


Web root

Detailed Errors
Determines if detailed errors should be captured.
Key: detailedErrors
Type: bool ( true or 1 )
Default: false
Set using: UseSetting
Environment variable: ASPNETCORE_DETAILEDERRORS

When enabled (or when the Environment is set to Development ), the app captures
detailed exceptions.

C#

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.DetailedErrorsKey, "true")

Environment
Sets the app's environment.

Key: environment
Type: string
Default: Production
Set using: UseEnvironment
Environment variable: ASPNETCORE_ENVIRONMENT

The environment can be set to any value. Framework-defined values include


Development , Staging , and Production . Values aren't case sensitive. By default, the

Environment is read from the ASPNETCORE_ENVIRONMENT environment variable. When using


Visual Studio , environment variables may be set in the launchSettings.json file. For
more information, see Use multiple environments in ASP.NET Core.

C#

WebHost.CreateDefaultBuilder(args)
.UseEnvironment(EnvironmentName.Development)

Hosting Startup Assemblies


Sets the app's hosting startup assemblies.

Key: hostingStartupAssemblies
Type: string
Default: Empty string
Set using: UseSetting
Environment variable: ASPNETCORE_HOSTINGSTARTUPASSEMBLIES

A semicolon-delimited string of hosting startup assemblies to load on startup.

Although the configuration value defaults to an empty string, the hosting startup
assemblies always include the app's assembly. When hosting startup assemblies are
provided, they're added to the app's assembly for loading when the app builds its
common services during startup.

C#

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey,
"assembly1;assembly2")

HTTPS Port
Set the HTTPS redirect port. Used in enforcing HTTPS.

Key: https_port
Type: string
Default: A default value isn't set.
Set using: UseSetting
Environment variable: ASPNETCORE_HTTPS_PORT

C#

WebHost.CreateDefaultBuilder(args)
.UseSetting("https_port", "8080")

Hosting Startup Exclude Assemblies


A semicolon-delimited string of hosting startup assemblies to exclude on startup.

Key: hostingStartupExcludeAssemblies
Type: string
Default: Empty string
Set using: UseSetting
Environment variable: ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES

C#
WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey,
"assembly1;assembly2")

Prefer Hosting URLs


Indicates whether the host should listen on the URLs configured with the
WebHostBuilder instead of those configured with the IServer implementation.

Key: preferHostingUrls
Type: bool ( true or 1 )
Default: true
Set using: PreferHostingUrls
Environment variable: ASPNETCORE_PREFERHOSTINGURLS

C#

WebHost.CreateDefaultBuilder(args)
.PreferHostingUrls(false)

Prevent Hosting Startup


Prevents the automatic loading of hosting startup assemblies, including hosting startup
assemblies configured by the app's assembly. For more information, see Use hosting
startup assemblies in ASP.NET Core.

Key: preventHostingStartup
Type: bool ( true or 1 )
Default: false
Set using: UseSetting
Environment variable: ASPNETCORE_PREVENTHOSTINGSTARTUP

C#

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true")

Server URLs
Indicates the IP addresses or host addresses with ports and protocols that the server
should listen on for requests.

Key: urls
Type: string
Default: http://localhost:5000
Set using: UseUrls
Environment variable: ASPNETCORE_URLS

Set to a semicolon-separated (;) list of URL prefixes to which the server should respond.
For example, http://localhost:123 . Use "*" to indicate that the server should listen for
requests on any IP address or hostname using the specified port and protocol (for
example, http://*:5000 ). The protocol ( http:// or https:// ) must be included with
each URL. Supported formats vary among servers.

C#

WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002")

Kestrel has its own endpoint configuration API. For more information, see Configure
endpoints for the ASP.NET Core Kestrel web server.

Shutdown Timeout
Specifies the amount of time to wait for Web Host to shut down.

Key: shutdownTimeoutSeconds
Type: int
Default: 5
Set using: UseShutdownTimeout
Environment variable: ASPNETCORE_SHUTDOWNTIMEOUTSECONDS

Although the key accepts an int with UseSetting (for example,


.UseSetting(WebHostDefaults.ShutdownTimeoutKey, "10") ), the UseShutdownTimeout

extension method takes a TimeSpan.

During the timeout period, hosting:

Triggers IApplicationLifetime.ApplicationStopping.
Attempts to stop hosted services, logging any errors for services that fail to stop.
If the timeout period expires before all of the hosted services stop, any remaining active
services are stopped when the app shuts down. The services stop even if they haven't
finished processing. If services require additional time to stop, increase the timeout.

C#

WebHost.CreateDefaultBuilder(args)
.UseShutdownTimeout(TimeSpan.FromSeconds(10))

Startup Assembly
Determines the assembly to search for the Startup class.

Key: startupAssembly
Type: string
Default: The app's assembly
Set using: UseStartup
Environment variable: ASPNETCORE_STARTUPASSEMBLY

The assembly by name ( string ) or type ( TStartup ) can be referenced. If multiple


UseStartup methods are called, the last one takes precedence.

C#

WebHost.CreateDefaultBuilder(args)
.UseStartup("StartupAssemblyName")

C#

WebHost.CreateDefaultBuilder(args)
.UseStartup<TStartup>()

Web root
Sets the relative path to the app's static assets.

Key: webroot
Type: string
Default: The default is wwwroot . The path to {content root}/wwwroot must exist. If the
path doesn't exist, a no-op file provider is used.
Set using: UseWebRoot
Environment variable: ASPNETCORE_WEBROOT
C#

WebHost.CreateDefaultBuilder(args)
.UseWebRoot("public")

For more information, see:

Fundamentals: Web root


Content root

Override configuration
Use Configuration to configure Web Host. In the following example, host configuration
is optionally specified in a hostsettings.json file. Any configuration loaded from the
hostsettings.json file may be overridden by command-line arguments. The built

configuration (in config ) is used to configure the host with UseConfiguration.


IWebHostBuilder configuration is added to the app's configuration, but the converse
isn't true— ConfigureAppConfiguration doesn't affect the IWebHostBuilder configuration.

Overriding the configuration provided by UseUrls with hostsettings.json config first,


command-line argument config second:

C#

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args)


{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("hostsettings.json", optional: true)
.AddCommandLine(args)
.Build();

return WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000")
.UseConfiguration(config)
.Configure(app =>
{
app.Run(context =>
context.Response.WriteAsync("Hello, World!"));
});
}
}

hostsettings.json :

JSON

{
urls: "http://*:5005"
}

7 Note

UseConfiguration only copies keys from the provided IConfiguration to the host
builder configuration. Therefore, setting reloadOnChange: true for JSON, INI, and
XML settings files has no effect.

To specify the host run on a particular URL, the desired value can be passed in from a
command prompt when executing dotnet run. The command-line argument overrides
the urls value from the hostsettings.json file, and the server listens on port 8080:

.NET CLI

dotnet run --urls "http://*:8080"

Manage the host


Run

The Run method starts the web app and blocks the calling thread until the host is shut
down:

C#

host.Run();

Start

Run the host in a non-blocking manner by calling its Start method:

C#
using (host)
{
host.Start();
Console.ReadLine();
}

If a list of URLs is passed to the Start method, it listens on the URLs specified:

C#

var urls = new List<string>()


{
"http://*:5000",
"http://localhost:5001"
};

var host = new WebHostBuilder()


.UseKestrel()
.UseStartup<Startup>()
.Start(urls.ToArray());

using (host)
{
Console.ReadLine();
}

The app can initialize and start a new host using the pre-configured defaults of
CreateDefaultBuilder using a static convenience method. These methods start the
server without console output and with WaitForShutdown wait for a break (Ctrl-
C/SIGINT or SIGTERM):

Start(RequestDelegate app)

Start with a RequestDelegate :

C#

using (var host = WebHost.Start(app => app.Response.WriteAsync("Hello,


World!")))
{
Console.WriteLine("Use Ctrl-C to shutdown the host...");
host.WaitForShutdown();
}

Make a request in the browser to http://localhost:5000 to receive the response "Hello


World!" WaitForShutdown blocks until a break (Ctrl-C/SIGINT or SIGTERM) is issued. The
app displays the Console.WriteLine message and waits for a keypress to exit.
Start(string url, RequestDelegate app)

Start with a URL and RequestDelegate :

C#

using (var host = WebHost.Start("http://localhost:8080", app =>


app.Response.WriteAsync("Hello, World!")))
{
Console.WriteLine("Use Ctrl-C to shutdown the host...");
host.WaitForShutdown();
}

Produces the same result as Start(RequestDelegate app), except the app responds on
http://localhost:8080 .

Start(Action<IRouteBuilder> routeBuilder)

Use an instance of IRouteBuilder (Microsoft.AspNetCore.Routing ) to use routing


middleware:

C#

using (var host = WebHost.Start(router => router


.MapGet("hello/{name}", (req, res, data) =>
res.WriteAsync($"Hello, {data.Values["name"]}!"))
.MapGet("buenosdias/{name}", (req, res, data) =>
res.WriteAsync($"Buenos dias, {data.Values["name"]}!"))
.MapGet("throw/{message?}", (req, res, data) =>
throw new Exception((string)data.Values["message"] ?? "Uh oh!"))
.MapGet("{greeting}/{name}", (req, res, data) =>
res.WriteAsync($"{data.Values["greeting"]},
{data.Values["name"]}!"))
.MapGet("", (req, res, data) => res.WriteAsync("Hello, World!"))))
{
Console.WriteLine("Use Ctrl-C to shutdown the host...");
host.WaitForShutdown();
}

Use the following browser requests with the example:

Request Response

http://localhost:5000/hello/Martin Hello, Martin!

http://localhost:5000/buenosdias/Catrina Buenos dias, Catrina!

http://localhost:5000/throw/ooops! Throws an exception with string "ooops!"


Request Response

http://localhost:5000/throw Throws an exception with string "Uh oh!"

http://localhost:5000/Sante/Kevin Sante, Kevin!

http://localhost:5000 Hello World!

WaitForShutdown blocks until a break (Ctrl-C/SIGINT or SIGTERM) is issued. The app

displays the Console.WriteLine message and waits for a keypress to exit.

Start(string url, Action<IRouteBuilder> routeBuilder)

Use a URL and an instance of IRouteBuilder :

C#

using (var host = WebHost.Start("http://localhost:8080", router => router


.MapGet("hello/{name}", (req, res, data) =>
res.WriteAsync($"Hello, {data.Values["name"]}!"))
.MapGet("buenosdias/{name}", (req, res, data) =>
res.WriteAsync($"Buenos dias, {data.Values["name"]}!"))
.MapGet("throw/{message?}", (req, res, data) =>
throw new Exception((string)data.Values["message"] ?? "Uh oh!"))
.MapGet("{greeting}/{name}", (req, res, data) =>
res.WriteAsync($"{data.Values["greeting"]},
{data.Values["name"]}!"))
.MapGet("", (req, res, data) => res.WriteAsync("Hello, World!"))))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Produces the same result as Start(Action<IRouteBuilder> routeBuilder), except the app


responds at http://localhost:8080 .

StartWith(Action<IApplicationBuilder> app)

Provide a delegate to configure an IApplicationBuilder :

C#

using (var host = WebHost.StartWith(app =>


app.Use(next =>
{
return async context =>
{
await context.Response.WriteAsync("Hello World!");
};
})))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Make a request in the browser to http://localhost:5000 to receive the response "Hello


World!" WaitForShutdown blocks until a break (Ctrl-C/SIGINT or SIGTERM) is issued. The
app displays the Console.WriteLine message and waits for a keypress to exit.

StartWith(string url, Action<IApplicationBuilder> app)

Provide a URL and a delegate to configure an IApplicationBuilder :

C#

using (var host = WebHost.StartWith("http://localhost:8080", app =>


app.Use(next =>
{
return async context =>
{
await context.Response.WriteAsync("Hello World!");
};
})))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Produces the same result as StartWith(Action<IApplicationBuilder> app), except the


app responds on http://localhost:8080 .

IWebHostEnvironment interface
The IWebHostEnvironment interface provides information about the app's web hosting
environment. Use constructor injection to obtain the IWebHostEnvironment in order to
use its properties and extension methods:

C#

public class CustomFileReader


{
private readonly IWebHostEnvironment _env;

public CustomFileReader(IWebHostEnvironment env)


{
_env = env;
}

public string ReadFile(string filePath)


{
var fileProvider = _env.WebRootFileProvider;
// Process the file here
}
}

A convention-based approach can be used to configure the app at startup based on the
environment. Alternatively, inject the IWebHostEnvironment into the Startup constructor
for use in ConfigureServices :

C#

public class Startup


{
public Startup(IWebHostEnvironment env)
{
HostingEnvironment = env;
}

public IWebHostEnvironment HostingEnvironment { get; }

public void ConfigureServices(IServiceCollection services)


{
if (HostingEnvironment.IsDevelopment())
{
// Development configuration
}
else
{
// Staging/Production configuration
}

var contentRootPath = HostingEnvironment.ContentRootPath;


}
}

7 Note

In addition to the IsDevelopment extension method, IWebHostEnvironment offers


IsStaging , IsProduction , and IsEnvironment(string environmentName) methods.

For more information, see Use multiple environments in ASP.NET Core.

The IWebHostEnvironment service can also be injected directly into the Configure
method for setting up the processing pipeline:
C#

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
if (env.IsDevelopment())
{
// In Development, use the Developer Exception Page
app.UseDeveloperExceptionPage();
}
else
{
// In Staging/Production, route exceptions to /error
app.UseExceptionHandler("/error");
}

var contentRootPath = env.ContentRootPath;


}

IWebHostEnvironment can be injected into the Invoke method when creating custom

middleware:

C#

public async Task Invoke(HttpContext context, IWebHostEnvironment env)


{
if (env.IsDevelopment())
{
// Configure middleware for Development
}
else
{
// Configure middleware for Staging/Production
}

var contentRootPath = env.ContentRootPath;


}

IHostApplicationLifetime interface
IHostApplicationLifetime allows for post-startup and shutdown activities. Three
properties on the interface are cancellation tokens used to register Action methods that
define startup and shutdown events.

Cancellation Token Triggered when…

ApplicationStarted The host has fully started.


Cancellation Token Triggered when…

ApplicationStopped The host is completing a graceful shutdown. All requests should be


processed. Shutdown blocks until this event completes.

ApplicationStopping The host is performing a graceful shutdown. Requests may still be


processing. Shutdown blocks until this event completes.

C#

public class Startup


{
public void Configure(IApplicationBuilder app, IHostApplicationLifetime
appLifetime)
{
appLifetime.ApplicationStarted.Register(OnStarted);
appLifetime.ApplicationStopping.Register(OnStopping);
appLifetime.ApplicationStopped.Register(OnStopped);

Console.CancelKeyPress += (sender, eventArgs) =>


{
appLifetime.StopApplication();
// Don't terminate the process immediately, wait for the Main
thread to exit gracefully.
eventArgs.Cancel = true;
};
}

private void OnStarted()


{
// Perform post-startup activities here
}

private void OnStopping()


{
// Perform on-stopping activities here
}

private void OnStopped()


{
// Perform post-stopped activities here
}
}

StopApplication requests termination of the app. The following class uses

StopApplication to gracefully shut down an app when the class's Shutdown method is
called:

C#
public class MyClass
{
private readonly IHostApplicationLifetime _appLifetime;

public MyClass(IHostApplicationLifetime appLifetime)


{
_appLifetime = appLifetime;
}

public void Shutdown()


{
_appLifetime.StopApplication();
}
}

Scope validation
CreateDefaultBuilder sets ServiceProviderOptions.ValidateScopes to true if the app's
environment is Development.

When ValidateScopes is set to true , the default service provider performs checks to
verify that:

Scoped services aren't directly or indirectly resolved from the root service provider.
Scoped services aren't directly or indirectly injected into singletons.

The root service provider is created when BuildServiceProvider is called. The root service
provider's lifetime corresponds to the app/server's lifetime when the provider starts with
the app and is disposed when the app shuts down.

Scoped services are disposed by the container that created them. If a scoped service is
created in the root container, the service's lifetime is effectively promoted to singleton
because it's only disposed by the root container when app/server is shut down.
Validating service scopes catches these situations when BuildServiceProvider is called.

To always validate scopes, including in the Production environment, configure the


ServiceProviderOptions with UseDefaultServiceProvider on the host builder:

C#

WebHost.CreateDefaultBuilder(args)
.UseDefaultServiceProvider((context, options) => {
options.ValidateScopes = true;
})
Additional resources
Host ASP.NET Core on Windows with IIS
Host ASP.NET Core on Linux with Nginx
Host ASP.NET Core on Linux with Apache
Host ASP.NET Core in a Windows Service
Configuration in ASP.NET Core
Article • 01/15/2023

By Rick Anderson and Kirk Larkin

Application configuration in ASP.NET Core is performed using one or more


configuration providers. Configuration providers read configuration data from key-value
pairs using a variety of configuration sources:

Settings files, such as appsettings.json


Environment variables
Azure Key Vault
Azure App Configuration
Command-line arguments
Custom providers, installed or created
Directory files
In-memory .NET objects

This article provides information on configuration in ASP.NET Core. For information on


using configuration in console apps, see .NET Configuration.

Application and Host Configuration


ASP.NET Core apps configure and launch a host. The host is responsible for app startup
and lifetime management. The ASP.NET Core templates create a WebApplicationBuilder
which contains the host. While some configuration can be done in both the host and the
application configuration providers, generally, only configuration that is necessary for
the host should be done in host configuration.

Application configuration is the highest priority and is detailed in the next section. Host
configuration follows application configuration, and is described in this article.

Default application configuration sources


ASP.NET Core web apps created with dotnet new or Visual Studio generate the
following code:

C#

var builder = WebApplication.CreateBuilder(args);


WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder
class with preconfigured defaults. The initialized WebApplicationBuilder ( builder )
provides default configuration for the app in the following order, from highest to lowest
priority:

1. Command-line arguments using the Command-line configuration provider.


2. Non-prefixed environment variables using the Non-prefixed environment variables
configuration provider.
3. User secrets when the app runs in the Development environment.
4. appsettings.{Environment}.json using the JSON configuration provider. For
example, appsettings.Production.json and appsettings.Development.json .
5. appsettings.json using the JSON configuration provider.
6. A fallback to the host configuration described in the next section.

Default host configuration sources


The following list contains the default host configuration sources from highest to lowest
priority for WebApplicationBuilder:

1. Command-line arguments using the Command-line configuration provider


2. DOTNET_ -prefixed environment variables using the Environment variables
configuration provider.
3. ASPNETCORE_ -prefixed environment variables using the Environment variables
configuration provider.

For the .NET Generic Host and Web Host, the default host configuration sources from
highest to lowest priority is:

1. ASPNETCORE_ -prefixed environment variables using the Environment variables


configuration provider.
2. Command-line arguments using the Command-line configuration provider
3. DOTNET_ -prefixed environment variables using the Environment variables
configuration provider.

When a configuration value is set in host and application configuration, the application
configuration is used.

Host variables
The following variables are locked in early when initializing the host builders and can't
be influenced by application config:
Application name
Environment name, for example Development , Production , and Staging
Content root
Web root
Whether to scan for hosting startup assemblies and which assemblies to scan for.
Variables read by app and library code from HostBuilderContext.Configuration in
IHostBuilder.ConfigureAppConfiguration callbacks.

Every other host setting is read from application config instead of host config.

URLS is one of the many common host settings that is not a bootstrap setting. Like
every other host setting not in the previous list, URLS is read later from application
config. Host config is a fallback for application config, so host config can be used to set
URLS , but it will be overridden by any configuration source in application config like
appsettings.json .

For more information, see Change the content root, app name, and environment and
Change the content root, app name, and environment by environment variables or
command line

The remaining sections in this article refer to application configuration.

Application configuration providers


The following code displays the enabled configuration providers in the order they were
added:

C#

public class Index2Model : PageModel


{
private IConfigurationRoot ConfigRoot;

public Index2Model(IConfiguration configRoot)


{
ConfigRoot = (IConfigurationRoot)configRoot;
}

public ContentResult OnGet()


{
string str = "";
foreach (var provider in ConfigRoot.Providers.ToList())
{
str += provider.ToString() + "\n";
}
return Content(str);
}
}

The preceding list of highest to lowest priority default configuration sources shows the
providers in the opposite order they are added to template generated application. For
example, the JSON configuration provider is added before the Command-line
configuration provider.

Configuration providers that are added later have higher priority and override previous
key settings. For example, if MyKey is set in both appsettings.json and the environment,
the environment value is used. Using the default configuration providers, the
Command-line configuration provider overrides all other providers.

For more information on CreateBuilder , see Default builder settings.

appsettings.json

Consider the following appsettings.json file:

JSON

{
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
},
"MyKey": "My appsettings.json Value",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

The following code from the sample download displays several of the preceding
configurations settings:

C#

public class TestModel : PageModel


{
// requires using Microsoft.Extensions.Configuration;
private readonly IConfiguration Configuration;
public TestModel(IConfiguration configuration)
{
Configuration = configuration;
}

public ContentResult OnGet()


{
var myKeyValue = Configuration["MyKey"];
var title = Configuration["Position:Title"];
var name = Configuration["Position:Name"];
var defaultLogLevel = Configuration["Logging:LogLevel:Default"];

return Content($"MyKey value: {myKeyValue} \n" +


$"Title: {title} \n" +
$"Name: {name} \n" +
$"Default Log Level: {defaultLogLevel}");
}
}

The default JsonConfigurationProvider loads configuration in the following order:

1. appsettings.json
2. appsettings.{Environment}.json : For example, the appsettings.Production.json
and appsettings.Development.json files. The environment version of the file is
loaded based on the IHostingEnvironment.EnvironmentName. For more
information, see Use multiple environments in ASP.NET Core.

appsettings.{Environment}.json values override keys in appsettings.json . For example,

by default:

In development, appsettings.Development.json configuration overwrites values


found in appsettings.json .
In production, appsettings.Production.json configuration overwrites values found
in appsettings.json . For example, when deploying the app to Azure.

If a configuration value must be guaranteed, see GetValue. The preceding example only
reads strings and doesn’t support a default value.

Using the default configuration, the appsettings.json and appsettings.


{Environment}.json files are enabled with reloadOnChange: true . Changes made to
the appsettings.json and appsettings.{Environment}.json file after the app starts are
read by the JSON configuration provider.

Comments in appsettings.json
Comments in appsettings.json and appsettings.{Environment}.json files are supported
using JavaScript or C# style comments.

Bind hierarchical configuration data using the options


pattern
The preferred way to read related configuration values is using the options pattern. For
example, to read the following configuration values:

JSON

"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}

Create the following PositionOptions class:

C#

public class PositionOptions


{
public const string Position = "Position";

public string Title { get; set; } = String.Empty;


public string Name { get; set; } = String.Empty;
}

An options class:

Must be non-abstract with a public parameterless constructor.


All public read-write properties of the type are bound.
Fields are not bound. In the preceding code, Position is not bound. The Position
field is used so the string "Position" doesn't need to be hard coded in the app
when binding the class to a configuration provider.

The following code:

Calls ConfigurationBinder.Bind to bind the PositionOptions class to the Position


section.
Displays the Position configuration data.

C#
public class Test22Model : PageModel
{
private readonly IConfiguration Configuration;

public Test22Model(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
var positionOptions = new PositionOptions();

Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

return Content($"Title: {positionOptions.Title} \n" +


$"Name: {positionOptions.Name}");
}
}

In the preceding code, by default, changes to the JSON configuration file after the app
has started are read.

ConfigurationBinder.Get<T> binds and returns the specified type.


ConfigurationBinder.Get<T> may be more convenient than using

ConfigurationBinder.Bind . The following code shows how to use

ConfigurationBinder.Get<T> with the PositionOptions class:

C#

public class Test21Model : PageModel


{
private readonly IConfiguration Configuration;
public PositionOptions? positionOptions { get; private set; }

public Test21Model(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
positionOptions = Configuration.GetSection(PositionOptions.Position)
.Get<PositionOptions>
();

return Content($"Title: {positionOptions.Title} \n" +


$"Name: {positionOptions.Name}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app
has started are read.

An alternative approach when using the options pattern is to bind the Position section
and add it to the dependency injection service container. In the following code,
PositionOptions is added to the service container with Configure and bound to

configuration:

C#

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

Using the preceding code, the following code reads the position options:

C#

public class Test2Model : PageModel


{
private readonly PositionOptions _options;

public Test2Model(IOptions<PositionOptions> options)


{
_options = options.Value;
}

public ContentResult OnGet()


{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}

In the preceding code, changes to the JSON configuration file after the app has started
are not read. To read changes after the app has started, use IOptionsSnapshot.

Using the default configuration, the appsettings.json and appsettings.


{Environment}.json files are enabled with reloadOnChange: true . Changes made to
the appsettings.json and appsettings.{Environment}.json file after the app starts are
read by the JSON configuration provider.

See JSON configuration provider in this document for information on adding additional
JSON configuration files.

Combining service collection


Consider the following which registers services and configures options:

C#

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();

var app = builder.Build();

Related groups of registrations can be moved to an extension method to register


services. For example, the configuration services are added to the following class:

C#

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));
return services;
}

public static IServiceCollection AddMyDependencyGroup(


this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();

return services;
}
}
}

The remaining services are registered in a similar class. The following code uses the new
extension methods to register the services:

C#

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Note: Each services.Add{GROUP_NAME} extension method adds and potentially configures


services. For example, AddControllersWithViews adds the services MVC controllers with
views require, and AddRazorPages adds the services Razor Pages requires.

Security and user secrets


Configuration data guidelines:

Never store passwords or other sensitive data in configuration provider code or in


plain text configuration files. The Secret Manager tool can be used to store secrets
in development.
Don't use production secrets in development or test environments.
Specify secrets outside of the project so that they can't be accidentally committed
to a source code repository.
By default, the user secrets configuration source is registered after the JSON
configuration sources. Therefore, user secrets keys take precedence over keys in
appsettings.json and appsettings.{Environment}.json .

For more information on storing passwords or other sensitive data:

Use multiple environments in ASP.NET Core


Safe storage of app secrets in development in ASP.NET Core: Includes advice on
using environment variables to store sensitive data. The Secret Manager tool uses
the File configuration provider to store user secrets in a JSON file on the local
system.

Azure Key Vault safely stores app secrets for ASP.NET Core apps. For more
information, see Azure Key Vault configuration provider in ASP.NET Core.

Non-prefixed environment variables


Non-prefixed environment variables are environment variables other than those
prefixed by ASPNETCORE_ or DOTNET_ . For example, the ASP.NET Core web application
templates set "ASPNETCORE_ENVIRONMENT": "Development" in launchSettings.json . For
more information on ASPNETCORE_ and DOTNET_ environment variables, see:

List of highest to lowest priority default configuration sources including non-


prefixed, ASPNETCORE_ -prefixed and DOTNETCORE_ -prefixed environment variables.
DOTNET_ environment variables used outside of Microsoft.Extensions.Hosting.

Using the default configuration, the EnvironmentVariablesConfigurationProvider loads


configuration from environment variable key-value pairs after reading
appsettings.json , appsettings.{Environment}.json , and user secrets. Therefore, key

values read from the environment override values read from appsettings.json ,
appsettings.{Environment}.json , and user secrets.

The : separator doesn't work with environment variable hierarchical keys on all
platforms. __ , the double underscore, is:

Supported by all platforms. For example, the : separator is not supported by


Bash , but __ is.
Automatically replaced by a :

The following set commands:

Set the environment keys and values of the preceding example on Windows.
Test the settings when using the sample download . The dotnet run command
must be run in the project directory.

.NET CLI

set MyKey="My key from Environment"


set Position__Title=Environment_Editor
set Position__Name=Environment_Rick
dotnet run

The preceding environment settings:

Are only set in processes launched from the command window they were set in.
Won't be read by browsers launched with Visual Studio.

The following setx commands can be used to set the environment keys and values on
Windows. Unlike set , setx settings are persisted. /M sets the variable in the system
environment. If the /M switch isn't used, a user environment variable is set.

Console

setx MyKey "My key from setx Environment" /M


setx Position__Title Environment_Editor /M
setx Position__Name Environment_Rick /M

To test that the preceding commands override appsettings.json and appsettings.


{Environment}.json :

With Visual Studio: Exit and restart Visual Studio.


With the CLI: Start a new command window and enter dotnet run .

Call AddEnvironmentVariables with a string to specify a prefix for environment variables:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Configuration.AddEnvironmentVariables(prefix: "MyCustomPrefix_");

var app = builder.Build();

In the preceding code:


builder.Configuration.AddEnvironmentVariables(prefix: "MyCustomPrefix_") is

added after the default configuration providers. For an example of ordering the
configuration providers, see JSON configuration provider.
Environment variables set with the MyCustomPrefix_ prefix override the default
configuration providers. This includes environment variables without the prefix.

The prefix is stripped off when the configuration key-value pairs are read.

The following commands test the custom prefix:

.NET CLI

set MyCustomPrefix_MyKey="My key with MyCustomPrefix_ Environment"


set MyCustomPrefix_Position__Title=Editor_with_customPrefix
set MyCustomPrefix_Position__Name=Environment_Rick_cp
dotnet run

The default configuration loads environment variables and command line arguments
prefixed with DOTNET_ and ASPNETCORE_ . The DOTNET_ and ASPNETCORE_ prefixes are used
by ASP.NET Core for host and app configuration, but not for user configuration. For
more information on host and app configuration, see .NET Generic Host.

On Azure App Service , select New application setting on the Settings >
Configuration page. Azure App Service application settings are:

Encrypted at rest and transmitted over an encrypted channel.


Exposed as environment variables.

For more information, see Azure Apps: Override app configuration using the Azure
Portal.

See Connection string prefixes for information on Azure database connection strings.

Naming of environment variables


Environment variable names reflect the structure of an appsettings.json file. Each
element in the hierarchy is separated by a double underscore (preferable) or a colon.
When the element structure includes an array, the array index should be treated as an
additional element name in this path. Consider the following appsettings.json file and
its equivalent values represented as environment variables.

appsettings.json

JSON
{
"SmtpServer": "smtp.example.com",
"Logging": [
{
"Name": "ToEmail",
"Level": "Critical",
"Args": {
"FromAddress": "MySystem@example.com",
"ToAddress": "SRE@example.com"
}
},
{
"Name": "ToConsole",
"Level": "Information"
}
]
}

environment variables

Console

setx SmtpServer smtp.example.com


setx Logging__0__Name ToEmail
setx Logging__0__Level Critical
setx Logging__0__Args__FromAddress MySystem@example.com
setx Logging__0__Args__ToAddress SRE@example.com
setx Logging__1__Name ToConsole
setx Logging__1__Level Information

Environment variables set in generated


launchSettings.json
Environment variables set in launchSettings.json override those set in the system
environment. For example, the ASP.NET Core web templates generate a
launchSettings.json file that sets the endpoint configuration to:

JSON

"applicationUrl": "https://localhost:5001;http://localhost:5000"

Configuring the applicationUrl sets the ASPNETCORE_URLS environment variable and


overrides values set in the environment.

Escape environment variables on Linux


On Linux, the value of URL environment variables must be escaped so systemd can
parse it. Use the linux tool systemd-escape which yields http:--localhost:5001

Windows Command Prompt

groot@terminus:~$ systemd-escape http://localhost:5001


http:--localhost:5001

Display environment variables


The following code displays the environment variables and values on application
startup, which can be helpful when debugging environment settings:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

foreach (var c in builder.Configuration.AsEnumerable())


{
Console.WriteLine(c.Key + " = " + c.Value);
}

Command-line
Using the default configuration, the CommandLineConfigurationProvider loads
configuration from command-line argument key-value pairs after the following
configuration sources:

appsettings.json and appsettings.{Environment}.json files.

App secrets in the Development environment.


Environment variables.

By default, configuration values set on the command-line override configuration values


set with all the other configuration providers.

Command-line arguments
The following command sets keys and values using = :

.NET CLI

dotnet run MyKey="Using =" Position:Title=Cmd Position:Name=Cmd_Rick


The following command sets keys and values using / :

.NET CLI

dotnet run /MyKey "Using /" /Position:Title=Cmd /Position:Name=Cmd_Rick

The following command sets keys and values using -- :

.NET CLI

dotnet run --MyKey "Using --" --Position:Title=Cmd --Position:Name=Cmd_Rick

The key value:

Must follow = , or the key must have a prefix of -- or / when the value follows a
space.
Isn't required if = is used. For example, MySetting= .

Within the same command, don't mix command-line argument key-value pairs that use
= with key-value pairs that use a space.

Switch mappings
Switch mappings allow key name replacement logic. Provide a dictionary of switch
replacements to the AddCommandLine method.

When the switch mappings dictionary is used, the dictionary is checked for a key that
matches the key provided by a command-line argument. If the command-line key is
found in the dictionary, the dictionary value is passed back to set the key-value pair into
the app's configuration. A switch mapping is required for any command-line key
prefixed with a single dash ( - ).

Switch mappings dictionary key rules:

Switches must start with - or -- .


The switch mappings dictionary must not contain duplicate keys.

To use a switch mappings dictionary, pass it into the call to AddCommandLine :

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddRazorPages();

var switchMappings = new Dictionary<string, string>()


{
{ "-k1", "key1" },
{ "-k2", "key2" },
{ "--alt3", "key3" },
{ "--alt4", "key4" },
{ "--alt5", "key5" },
{ "--alt6", "key6" },
};

builder.Configuration.AddCommandLine(args, switchMappings);

var app = builder.Build();

Run the following command works to test key replacement:

.NET CLI

dotnet run -k1 value1 -k2 value2 --alt3=value2 /alt4=value3 --alt5 value5
/alt6 value6

The following code shows the key values for the replaced keys:

C#

public class Test3Model : PageModel


{
private readonly IConfiguration Config;

public Test3Model(IConfiguration configuration)


{
Config = configuration;
}

public ContentResult OnGet()


{
return Content(
$"Key1: '{Config["Key1"]}'\n" +
$"Key2: '{Config["Key2"]}'\n" +
$"Key3: '{Config["Key3"]}'\n" +
$"Key4: '{Config["Key4"]}'\n" +
$"Key5: '{Config["Key5"]}'\n" +
$"Key6: '{Config["Key6"]}'");
}
}
For apps that use switch mappings, the call to CreateDefaultBuilder shouldn't pass
arguments. The CreateDefaultBuilder method's AddCommandLine call doesn't include
mapped switches, and there's no way to pass the switch-mapping dictionary to
CreateDefaultBuilder . The solution isn't to pass the arguments to CreateDefaultBuilder
but instead to allow the ConfigurationBuilder method's AddCommandLine method to
process both the arguments and the switch-mapping dictionary.

Set environment and command-line arguments


with Visual Studio
Environment and command-line arguments can be set in Visual Studio from the launch
profiles dialog:

In Solution Explorer, right click the project and select Properties.


Select the Debug > General tab and select Open debug launch profiles UI.

Hierarchical configuration data


The Configuration API reads hierarchical configuration data by flattening the hierarchical
data with the use of a delimiter in the configuration keys.

The sample download contains the following appsettings.json file:

JSON

{
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
},
"MyKey": "My appsettings.json Value",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

The following code from the sample download displays several of the configurations
settings:
C#

public class TestModel : PageModel


{
// requires using Microsoft.Extensions.Configuration;
private readonly IConfiguration Configuration;

public TestModel(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
var myKeyValue = Configuration["MyKey"];
var title = Configuration["Position:Title"];
var name = Configuration["Position:Name"];
var defaultLogLevel = Configuration["Logging:LogLevel:Default"];

return Content($"MyKey value: {myKeyValue} \n" +


$"Title: {title} \n" +
$"Name: {name} \n" +
$"Default Log Level: {defaultLogLevel}");
}
}

The preferred way to read hierarchical configuration data is using the options pattern.
For more information, see Bind hierarchical configuration data in this document.

GetSection and GetChildren methods are available to isolate sections and children of a
section in the configuration data. These methods are described later in GetSection,
GetChildren, and Exists.

Configuration keys and values


Configuration keys:

Are case-insensitive. For example, ConnectionString and connectionstring are


treated as equivalent keys.
If a key and value is set in more than one configuration provider, the value from
the last provider added is used. For more information, see Default configuration.
Hierarchical keys
Within the Configuration API, a colon separator ( : ) works on all platforms.
In environment variables, a colon separator may not work on all platforms. A
double underscore, __ , is supported by all platforms and is automatically
converted into a colon : .
In Azure Key Vault, hierarchical keys use -- as a separator. The Azure Key Vault
configuration provider automatically replaces -- with a : when the secrets are
loaded into the app's configuration.
The ConfigurationBinder supports binding arrays to objects using array indices in
configuration keys. Array binding is described in the Bind an array to a class
section.

Configuration values:

Are strings.
Null values can't be stored in configuration or bound to objects.

Configuration providers
The following table shows the configuration providers available to ASP.NET Core apps.

Provider Provides configuration from

Azure Key Vault configuration provider Azure Key Vault

Azure App configuration provider Azure App Configuration

Command-line configuration provider Command-line parameters

Custom configuration provider Custom source

Environment Variables configuration provider Environment variables

File configuration provider INI, JSON, and XML files

Key-per-file configuration provider Directory files

Memory configuration provider In-memory collections

User secrets File in the user profile directory

Configuration sources are read in the order that their configuration providers are
specified. Order configuration providers in code to suit the priorities for the underlying
configuration sources that the app requires.

A typical sequence of configuration providers is:

1. appsettings.json
2. appsettings.{Environment}.json
3. User secrets
4. Environment variables using the Environment Variables configuration provider.
5. Command-line arguments using the Command-line configuration provider.

A common practice is to add the Command-line configuration provider last in a series of


providers to allow command-line arguments to override configuration set by the other
providers.

The preceding sequence of providers is used in the default configuration.

Connection string prefixes


The Configuration API has special processing rules for four connection string
environment variables. These connection strings are involved in configuring Azure
connection strings for the app environment. Environment variables with the prefixes
shown in the table are loaded into the app with the default configuration or when no
prefix is supplied to AddEnvironmentVariables .

Connection string prefix Provider

CUSTOMCONNSTR_ Custom provider

MYSQLCONNSTR_ MySQL

SQLAZURECONNSTR_ Azure SQL Database

SQLCONNSTR_ SQL Server

When an environment variable is discovered and loaded into configuration with any of
the four prefixes shown in the table:

The configuration key is created by removing the environment variable prefix and
adding a configuration key section ( ConnectionStrings ).
A new configuration key-value pair is created that represents the database
connection provider (except for CUSTOMCONNSTR_ , which has no stated provider).

Environment variable Converted configuration Provider configuration entry


key key

CUSTOMCONNSTR_{KEY} ConnectionStrings:{KEY} Configuration entry not created.

MYSQLCONNSTR_{KEY} ConnectionStrings:{KEY} Key: ConnectionStrings:


{KEY}_ProviderName :
Value: MySql.Data.MySqlClient

SQLAZURECONNSTR_{KEY} ConnectionStrings:{KEY} Key: ConnectionStrings:


{KEY}_ProviderName :
Value: System.Data.SqlClient
Environment variable Converted configuration Provider configuration entry
key key

SQLCONNSTR_{KEY} ConnectionStrings:{KEY} Key: ConnectionStrings:


{KEY}_ProviderName :
Value: System.Data.SqlClient

File configuration provider


FileConfigurationProvider is the base class for loading configuration from the file
system. The following configuration providers derive from FileConfigurationProvider :

INI configuration provider


JSON configuration provider
XML configuration provider

INI configuration provider


The IniConfigurationProvider loads configuration from INI file key-value pairs at runtime.

The following code adds several configuration providers:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Configuration
.AddIniFile("MyIniConfig.ini", optional: true, reloadOnChange: true)
.AddIniFile($"MyIniConfig.{builder.Environment.EnvironmentName}.ini",
optional: true, reloadOnChange: true);

builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddCommandLine(args);

builder.Services.AddRazorPages();

var app = builder.Build();

In the preceding code, settings in the MyIniConfig.ini and MyIniConfig.


{Environment}.ini files are overridden by settings in the:

Environment variables configuration provider


Command-line configuration provider.

The sample download contains the following MyIniConfig.ini file:


ini

MyKey="MyIniConfig.ini Value"

[Position]
Title="My INI Config title"
Name="My INI Config name"

[Logging:LogLevel]
Default=Information
Microsoft=Warning

The following code from the sample download displays several of the preceding
configurations settings:

C#

public class TestModel : PageModel


{
// requires using Microsoft.Extensions.Configuration;
private readonly IConfiguration Configuration;

public TestModel(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
var myKeyValue = Configuration["MyKey"];
var title = Configuration["Position:Title"];
var name = Configuration["Position:Name"];
var defaultLogLevel = Configuration["Logging:LogLevel:Default"];

return Content($"MyKey value: {myKeyValue} \n" +


$"Title: {title} \n" +
$"Name: {name} \n" +
$"Default Log Level: {defaultLogLevel}");
}
}

JSON configuration provider


The JsonConfigurationProvider loads configuration from JSON file key-value pairs.

Overloads can specify:

Whether the file is optional.


Whether the configuration is reloaded if the file changes.

Consider the following code:

C#

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("MyConfig.json",
optional: true,
reloadOnChange: true);

builder.Services.AddRazorPages();

var app = builder.Build();

The preceding code:

Configures the JSON configuration provider to load the MyConfig.json file with the
following options:
optional: true : The file is optional.
reloadOnChange: true : The file is reloaded when changes are saved.

Reads the default configuration providers before the MyConfig.json file. Settings in
the MyConfig.json file override setting in the default configuration providers,
including the Environment variables configuration provider and the Command-line
configuration provider.

You typically don't want a custom JSON file overriding values set in the Environment
variables configuration provider and the Command-line configuration provider.

XML configuration provider


The XmlConfigurationProvider loads configuration from XML file key-value pairs at
runtime.

The following code adds several configuration providers:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Configuration
.AddXmlFile("MyXMLFile.xml", optional: true, reloadOnChange: true)
.AddXmlFile($"MyXMLFile.{builder.Environment.EnvironmentName}.xml",
optional: true, reloadOnChange: true);

builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddCommandLine(args);

builder.Services.AddRazorPages();

var app = builder.Build();

In the preceding code, settings in the MyXMLFile.xml and MyXMLFile.{Environment}.xml


files are overridden by settings in the:

Environment variables configuration provider


Command-line configuration provider.

The sample download contains the following MyXMLFile.xml file:

XML

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


<configuration>
<MyKey>MyXMLFile Value</MyKey>
<Position>
<Title>Title from MyXMLFile</Title>
<Name>Name from MyXMLFile</Name>
</Position>
<Logging>
<LogLevel>
<Default>Information</Default>
<Microsoft>Warning</Microsoft>
</LogLevel>
</Logging>
</configuration>

The following code from the sample download displays several of the preceding
configurations settings:

C#

public class TestModel : PageModel


{
// requires using Microsoft.Extensions.Configuration;
private readonly IConfiguration Configuration;

public TestModel(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
var myKeyValue = Configuration["MyKey"];
var title = Configuration["Position:Title"];
var name = Configuration["Position:Name"];
var defaultLogLevel = Configuration["Logging:LogLevel:Default"];

return Content($"MyKey value: {myKeyValue} \n" +


$"Title: {title} \n" +
$"Name: {name} \n" +
$"Default Log Level: {defaultLogLevel}");
}
}

Repeating elements that use the same element name work if the name attribute is used
to distinguish the elements:

XML

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


<configuration>
<section name="section0">
<key name="key0">value 00</key>
<key name="key1">value 01</key>
</section>
<section name="section1">
<key name="key0">value 10</key>
<key name="key1">value 11</key>
</section>
</configuration>

The following code reads the previous configuration file and displays the keys and
values:

C#

public class IndexModel : PageModel


{
private readonly IConfiguration Configuration;

public IndexModel(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
var key00 = "section:section0:key:key0";
var key01 = "section:section0:key:key1";
var key10 = "section:section1:key:key0";
var key11 = "section:section1:key:key1";
var val00 = Configuration[key00];
var val01 = Configuration[key01];
var val10 = Configuration[key10];
var val11 = Configuration[key11];

return Content($"{key00} value: {val00} \n" +


$"{key01} value: {val01} \n" +
$"{key10} value: {val10} \n" +
$"{key10} value: {val11} \n"
);
}
}

Attributes can be used to supply values:

XML

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


<configuration>
<key attribute="value" />
<section>
<key attribute="value" />
</section>
</configuration>

The previous configuration file loads the following keys with value :

key:attribute
section:key:attribute

Key-per-file configuration provider


The KeyPerFileConfigurationProvider uses a directory's files as configuration key-value
pairs. The key is the file name. The value contains the file's contents. The Key-per-file
configuration provider is used in Docker hosting scenarios.

To activate key-per-file configuration, call the AddKeyPerFile extension method on an


instance of ConfigurationBuilder. The directoryPath to the files must be an absolute
path.

Overloads permit specifying:

An Action<KeyPerFileConfigurationSource> delegate that configures the source.


Whether the directory is optional and the path to the directory.
The double-underscore ( __ ) is used as a configuration key delimiter in file names. For
example, the file name Logging__LogLevel__System produces the configuration key
Logging:LogLevel:System .

Call ConfigureAppConfiguration when building the host to specify the app's


configuration:

C#

.ConfigureAppConfiguration((hostingContext, config) =>


{
var path = Path.Combine(
Directory.GetCurrentDirectory(), "path/to/files");
config.AddKeyPerFile(directoryPath: path, optional: true);
})

Memory configuration provider


The MemoryConfigurationProvider uses an in-memory collection as configuration key-
value pairs.

The following code adds a memory collection to the configuration system:

C#

var builder = WebApplication.CreateBuilder(args);

var Dict = new Dictionary<string, string>


{
{"MyKey", "Dictionary MyKey Value"},
{"Position:Title", "Dictionary_Title"},
{"Position:Name", "Dictionary_Name" },
{"Logging:LogLevel:Default", "Warning"}
};

builder.Configuration.AddInMemoryCollection(Dict);
builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddCommandLine(args);

builder.Services.AddRazorPages();

var app = builder.Build();

The following code from the sample download displays the preceding configurations
settings:

C#
public class TestModel : PageModel
{
// requires using Microsoft.Extensions.Configuration;
private readonly IConfiguration Configuration;

public TestModel(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
var myKeyValue = Configuration["MyKey"];
var title = Configuration["Position:Title"];
var name = Configuration["Position:Name"];
var defaultLogLevel = Configuration["Logging:LogLevel:Default"];

return Content($"MyKey value: {myKeyValue} \n" +


$"Title: {title} \n" +
$"Name: {name} \n" +
$"Default Log Level: {defaultLogLevel}");
}
}

In the preceding code, config.AddInMemoryCollection(Dict) is added after the default


configuration providers. For an example of ordering the configuration providers, see
JSON configuration provider.

See Bind an array for another example using MemoryConfigurationProvider .

Kestrel endpoint configuration


Kestrel specific endpoint configuration overrides all cross-server endpoint
configurations. Cross-server endpoint configurations include:

UseUrls
--urls on the command line

The environment variable ASPNETCORE_URLS

Consider the following appsettings.json file used in an ASP.NET Core web app:

JSON
{
"Kestrel": {
"Endpoints": {
"Https": {
"Url": "https://localhost:9999"
}
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

When the preceding highlighted markup is used in an ASP.NET Core web app and the
app is launched on the command line with the following cross-server endpoint
configuration:

dotnet run --urls="https://localhost:7777"

Kestrel binds to the endpoint configured specifically for Kestrel in the appsettings.json
file ( https://localhost:9999 ) and not https://localhost:7777 .

Consider the Kestrel specific endpoint configured as an environment variable:

set Kestrel__Endpoints__Https__Url=https://localhost:8888

In the preceding environment variable, Https is the name of the Kestrel specific
endpoint. The preceding appsettings.json file also defines a Kestrel specific endpoint
named Https . By default, environment variables using the Environment Variables
configuration provider are read after appsettings.{Environment}.json , therefore, the
preceding environment variable is used for the Https endpoint.

GetValue
ConfigurationBinder.GetValue extracts a single value from configuration with a specified
key and converts it to the specified type:

C#

public class TestNumModel : PageModel


{
private readonly IConfiguration Configuration;

public TestNumModel(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
var number = Configuration.GetValue<int>("NumberKey", 99);
return Content($"{number}");
}
}

In the preceding code, if NumberKey isn't found in the configuration, the default value of
99 is used.

GetSection, GetChildren, and Exists


For the examples that follow, consider the following MySubsection.json file:

JSON

{
"section0": {
"key0": "value00",
"key1": "value01"
},
"section1": {
"key0": "value10",
"key1": "value11"
},
"section2": {
"subsection0": {
"key0": "value200",
"key1": "value201"
},
"subsection1": {
"key0": "value210",
"key1": "value211"
}
}
}

The following code adds MySubsection.json to the configuration providers:

C#
var builder = WebApplication.CreateBuilder(args);

builder.Configuration
.AddJsonFile("MySubsection.json",
optional: true,
reloadOnChange: true);

builder.Services.AddRazorPages();

var app = builder.Build();

GetSection
IConfiguration.GetSection returns a configuration subsection with the specified
subsection key.

The following code returns values for section1 :

C#

public class TestSectionModel : PageModel


{
private readonly IConfiguration Config;

public TestSectionModel(IConfiguration configuration)


{
Config = configuration.GetSection("section1");
}

public ContentResult OnGet()


{
return Content(
$"section1:key0: '{Config["key0"]}'\n" +
$"section1:key1: '{Config["key1"]}'");
}
}

The following code returns values for section2:subsection0 :

C#

public class TestSection2Model : PageModel


{
private readonly IConfiguration Config;

public TestSection2Model(IConfiguration configuration)


{
Config = configuration.GetSection("section2:subsection0");
}
public ContentResult OnGet()
{
return Content(
$"section2:subsection0:key0 '{Config["key0"]}'\n" +
$"section2:subsection0:key1:'{Config["key1"]}'");
}
}

GetSection never returns null . If a matching section isn't found, an empty


IConfigurationSection is returned.

When GetSection returns a matching section, Value isn't populated. A Key and Path are
returned when the section exists.

GetChildren and Exists


The following code calls IConfiguration.GetChildren and returns values for
section2:subsection0 :

C#

public class TestSection4Model : PageModel


{
private readonly IConfiguration Config;

public TestSection4Model(IConfiguration configuration)


{
Config = configuration;
}

public ContentResult OnGet()


{
string s = "";
var selection = Config.GetSection("section2");
if (!selection.Exists())
{
throw new Exception("section2 does not exist.");
}
var children = selection.GetChildren();

foreach (var subSection in children)


{
int i = 0;
var key1 = subSection.Key + ":key" + i++.ToString();
var key2 = subSection.Key + ":key" + i.ToString();
s += key1 + " value: " + selection[key1] + "\n";
s += key2 + " value: " + selection[key2] + "\n";
}
return Content(s);
}
}

The preceding code calls ConfigurationExtensions.Exists to verify the section exists:

Bind an array
The ConfigurationBinder.Bind supports binding arrays to objects using array indices in
configuration keys. Any array format that exposes a numeric key segment is capable of
array binding to a POCO class array.

Consider MyArray.json from the sample download :

JSON

{
"array": {
"entries": {
"0": "value00",
"1": "value10",
"2": "value20",
"4": "value40",
"5": "value50"
}
}
}

The following code adds MyArray.json to the configuration providers:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Configuration
.AddJsonFile("MyArray.json",
optional: true,
reloadOnChange: true);

builder.Services.AddRazorPages();

var app = builder.Build();

The following code reads the configuration and displays the values:

C#
public class ArrayModel : PageModel
{
private readonly IConfiguration Config;
public ArrayExample? _array { get; private set; }

public ArrayModel(IConfiguration config)


{
Config = config;
}

public ContentResult OnGet()


{
_array = Config.GetSection("array").Get<ArrayExample>();
if (_array == null)
{
throw new ArgumentNullException(nameof(_array));
}
string s = String.Empty;

for (int j = 0; j < _array.Entries.Length; j++)


{
s += $"Index: {j} Value: {_array.Entries[j]} \n";
}

return Content(s);
}
}

C#

public class ArrayExample


{
public string[]? Entries { get; set; }
}

The preceding code returns the following output:

text

Index: 0 Value: value00


Index: 1 Value: value10
Index: 2 Value: value20
Index: 3 Value: value40
Index: 4 Value: value50

In the preceding output, Index 3 has value value40 , corresponding to "4": "value40",
in MyArray.json . The bound array indices are continuous and not bound to the
configuration key index. The configuration binder isn't capable of binding null values or
creating null entries in bound objects.

Custom configuration provider


The sample app demonstrates how to create a basic configuration provider that reads
configuration key-value pairs from a database using Entity Framework (EF).

The provider has the following characteristics:

The EF in-memory database is used for demonstration purposes. To use a database


that requires a connection string, implement a secondary ConfigurationBuilder to
supply the connection string from another configuration provider.
The provider reads a database table into configuration at startup. The provider
doesn't query the database on a per-key basis.
Reload-on-change isn't implemented, so updating the database after the app
starts has no effect on the app's configuration.

Define an EFConfigurationValue entity for storing configuration values in the database.

Models/EFConfigurationValue.cs :

C#

public class EFConfigurationValue


{
public string Id { get; set; } = String.Empty;
public string Value { get; set; } = String.Empty;
}

Add an EFConfigurationContext to store and access the configured values.

EFConfigurationProvider/EFConfigurationContext.cs :

C#

public class EFConfigurationContext : DbContext


{
public EFConfigurationContext(DbContextOptions<EFConfigurationContext>
options) : base(options)
{
}

public DbSet<EFConfigurationValue> Values => Set<EFConfigurationValue>


();
}
Create a class that implements IConfigurationSource.

EFConfigurationProvider/EFConfigurationSource.cs :

C#

public class EFConfigurationSource : IConfigurationSource


{
private readonly Action<DbContextOptionsBuilder> _optionsAction;

public EFConfigurationSource(Action<DbContextOptionsBuilder>
optionsAction) => _optionsAction = optionsAction;

public IConfigurationProvider Build(IConfigurationBuilder builder) =>


new EFConfigurationProvider(_optionsAction);
}

Create the custom configuration provider by inheriting from ConfigurationProvider. The


configuration provider initializes the database when it's empty. Since configuration keys
are case-insensitive, the dictionary used to initialize the database is created with the
case-insensitive comparer (StringComparer.OrdinalIgnoreCase).

EFConfigurationProvider/EFConfigurationProvider.cs :

C#

public class EFConfigurationProvider : ConfigurationProvider


{
public EFConfigurationProvider(Action<DbContextOptionsBuilder>
optionsAction)
{
OptionsAction = optionsAction;
}

Action<DbContextOptionsBuilder> OptionsAction { get; }

public override void Load()


{
var builder = new DbContextOptionsBuilder<EFConfigurationContext>();

OptionsAction(builder);

using (var dbContext = new EFConfigurationContext(builder.Options))


{
if (dbContext == null || dbContext.Values == null)
{
throw new Exception("Null DB context");
}
dbContext.Database.EnsureCreated();
Data = !dbContext.Values.Any()
? CreateAndSaveDefaultValues(dbContext)
: dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
}
}

private static IDictionary<string, string> CreateAndSaveDefaultValues(


EFConfigurationContext dbContext)
{
// Quotes (c)2005 Universal Pictures: Serenity
// https://www.uphe.com/movies/serenity-2005
var configValues =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "quote1", "I aim to misbehave." },
{ "quote2", "I swallowed a bug." },
{ "quote3", "You can't stop the signal, Mal." }
};

if (dbContext == null || dbContext.Values == null)


{
throw new Exception("Null DB context");
}

dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());

dbContext.SaveChanges();

return configValues;
}
}

An AddEFConfiguration extension method permits adding the configuration source to a


ConfigurationBuilder .

Extensions/EntityFrameworkExtensions.cs :

C#

public static class EntityFrameworkExtensions


{
public static IConfigurationBuilder AddEFConfiguration(
this IConfigurationBuilder builder,
Action<DbContextOptionsBuilder> optionsAction)
{
return builder.Add(new EFConfigurationSource(optionsAction));
}
}

The following code shows how to use the custom EFConfigurationProvider in


Program.cs :

C#

//using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddEFConfiguration(
opt => opt.UseInMemoryDatabase("InMemoryDb"));

var app = builder.Build();

app.Run();

Access configuration with Dependency


Injection (DI)
Configuration can be injected into services using Dependency Injection (DI) by resolving
the IConfiguration service:

C#

public class Service


{
private readonly IConfiguration _config;

public Service(IConfiguration config) =>


_config = config;

public void DoSomething()


{
var configSettingValue = _config["ConfigSetting"];

// ...
}
}

For information on how to access values using IConfiguration , see GetValue and
GetSection, GetChildren, and Exists in this article.
Access configuration in Razor Pages
The following code displays configuration data in a Razor Page:

CSHTML

@page
@model Test5Model
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

Configuration value for 'MyKey': @Configuration["MyKey"]

In the following code, MyOptions is added to the service container with Configure and
bound to configuration:

C#

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

The following markup uses the @inject Razor directive to resolve and display the
options values:

CSHTML

@page
@model SampleApp.Pages.Test3Model
@using Microsoft.Extensions.Options
@using SampleApp.Models
@inject IOptions<MyOptions> optionsAccessor

<p><b>Option1:</b> @optionsAccessor.Value.Option1</p>
<p><b>Option2:</b> @optionsAccessor.Value.Option2</p>

Access configuration in a MVC view file


The following code displays configuration data in a MVC view:

CSHTML

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

Configuration value for 'MyKey': @Configuration["MyKey"]

Access configuration in Program.cs


The following code accesses configuration in the Program.cs file.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

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

var key1 = app.Configuration.GetValue<int>("KeyOne");


var key2 = app.Configuration.GetValue<bool>("KeyTwo");

app.Logger.LogInformation($"KeyOne = {key1}");
app.Logger.LogInformation($"KeyTwo = {key2}");

app.Run();

Configure options with a delegate


Options configured in a delegate override values set in the configuration providers.

In the following code, an IConfigureOptions<TOptions> service is added to the service


container. It uses a delegate to configure values for MyOptions :

C#

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(myOptions =>
{
myOptions.Option1 = "Value configured in delegate";
myOptions.Option2 = 500;
});

var app = builder.Build();

The following code displays the options values:

C#

public class Test2Model : PageModel


{
private readonly IOptions<MyOptions> _optionsDelegate;

public Test2Model(IOptions<MyOptions> optionsDelegate )


{
_optionsDelegate = optionsDelegate;
}

public ContentResult OnGet()


{
return Content($"Option1: {_optionsDelegate.Value.Option1} \n" +
$"Option2: {_optionsDelegate.Value.Option2}");
}
}

In the preceding example, the values of Option1 and Option2 are specified in
appsettings.json and then overridden by the configured delegate.

Host versus app configuration


Before the app is configured and started, a host is configured and launched. The host is
responsible for app startup and lifetime management. Both the app and the host are
configured using the configuration providers described in this topic. Host configuration
key-value pairs are also included in the app's configuration. For more information on
how the configuration providers are used when the host is built and how configuration
sources affect host configuration, see ASP.NET Core fundamentals overview.

Default host configuration


For details on the default configuration when using the Web Host, see the ASP.NET Core
2.2 version of this topic.

Host configuration is provided from:


Environment variables prefixed with DOTNET_ (for example, DOTNET_ENVIRONMENT )
using the Environment Variables configuration provider. The prefix ( DOTNET_ ) is
stripped when the configuration key-value pairs are loaded.
Command-line arguments using the Command-line configuration provider.
Web Host default configuration is established ( ConfigureWebHostDefaults ):
Kestrel is used as the web server and configured using the app's configuration
providers.
Add Host Filtering Middleware.
Add Forwarded Headers Middleware if the
ASPNETCORE_FORWARDEDHEADERS_ENABLED environment variable is set to true .

Enable IIS integration.

Other configuration
This topic only pertains to app configuration. Other aspects of running and hosting
ASP.NET Core apps are configured using configuration files not covered in this topic:

launch.json / launchSettings.json are tooling configuration files for the

Development environment, described:


In Use multiple environments in ASP.NET Core.
Across the documentation set where the files are used to configure ASP.NET
Core apps for Development scenarios.
web.config is a server configuration file, described in the following topics:

Host ASP.NET Core on Windows with IIS


ASP.NET Core Module (ANCM) for IIS

Environment variables set in launchSettings.json override those set in the system


environment.

For more information on migrating app configuration from earlier versions of ASP.NET,
see Update from ASP.NET to ASP.NET Core.

Add configuration from an external assembly


An IHostingStartup implementation allows adding enhancements to an app at startup
from an external assembly outside of the app's Startup class. For more information, see
Use hosting startup assemblies in ASP.NET Core.

Additional resources
Configuration source code
WebApplicationBuilder source code
View or download sample code (how to download)
Options pattern in ASP.NET Core
ASP.NET Core Blazor configuration
Options pattern in ASP.NET Core
Article • 06/03/2022

By Kirk Larkin and Rick Anderson .

The options pattern uses classes to provide strongly typed access to groups of related
settings. When configuration settings are isolated by scenario into separate classes, the
app adheres to two important software engineering principles:

Encapsulation:
Classes that depend on configuration settings depend only on the configuration
settings that they use.
Separation of Concerns:
Settings for different parts of the app aren't dependent or coupled to one
another.

Options also provide a mechanism to validate configuration data. For more information,
see the Options validation section.

This article provides information on the options pattern in ASP.NET Core. For
information on using the options pattern in console apps, see Options pattern in .NET.

Bind hierarchical configuration


The preferred way to read related configuration values is using the options pattern. For
example, to read the following configuration values:

JSON

"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}

Create the following PositionOptions class:

C#

public class PositionOptions


{
public const string Position = "Position";

public string Title { get; set; } = String.Empty;


public string Name { get; set; } = String.Empty;
}

An options class:

Must be non-abstract with a public parameterless constructor.


All public read-write properties of the type are bound.
Fields are not bound. In the preceding code, Position is not bound. The Position
field is used so the string "Position" doesn't need to be hard coded in the app
when binding the class to a configuration provider.

The following code:

Calls ConfigurationBinder.Bind to bind the PositionOptions class to the Position


section.
Displays the Position configuration data.

C#

public class Test22Model : PageModel


{
private readonly IConfiguration Configuration;

public Test22Model(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
var positionOptions = new PositionOptions();

Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

return Content($"Title: {positionOptions.Title} \n" +


$"Name: {positionOptions.Name}");
}
}

In the preceding code, by default, changes to the JSON configuration file after the app
has started are read.

ConfigurationBinder.Get<T> binds and returns the specified type.


ConfigurationBinder.Get<T> may be more convenient than using

ConfigurationBinder.Bind . The following code shows how to use

ConfigurationBinder.Get<T> with the PositionOptions class:


C#

public class Test21Model : PageModel


{
private readonly IConfiguration Configuration;
public PositionOptions? positionOptions { get; private set; }

public Test21Model(IConfiguration configuration)


{
Configuration = configuration;
}

public ContentResult OnGet()


{
positionOptions = Configuration.GetSection(PositionOptions.Position)
.Get<PositionOptions>
();

return Content($"Title: {positionOptions.Title} \n" +


$"Name: {positionOptions.Name}");
}
}

In the preceding code, by default, changes to the JSON configuration file after the app
has started are read.

An alternative approach when using the options pattern is to bind the Position section
and add it to the dependency injection service container. In the following code,
PositionOptions is added to the service container with Configure and bound to
configuration:

C#

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

Using the preceding code, the following code reads the position options:

C#
public class Test2Model : PageModel
{
private readonly PositionOptions _options;

public Test2Model(IOptions<PositionOptions> options)


{
_options = options.Value;
}

public ContentResult OnGet()


{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}

In the preceding code, changes to the JSON configuration file after the app has started
are not read. To read changes after the app has started, use IOptionsSnapshot.

Options interfaces
IOptions<TOptions>:

Does not support:


Reading of configuration data after the app has started.
Named options
Is registered as a Singleton and can be injected into any service lifetime.

IOptionsSnapshot<TOptions>:

Is useful in scenarios where options should be recomputed on every request. For


more information, see Use IOptionsSnapshot to read updated data.
Is registered as Scoped and therefore can't be injected into a Singleton service.
Supports named options

IOptionsMonitor<TOptions>:

Is used to retrieve options and manage options notifications for TOptions


instances.
Is registered as a Singleton and can be injected into any service lifetime.
Supports:
Change notifications
named options
Reloadable configuration
Selective options invalidation (IOptionsMonitorCache<TOptions>)
Post-configuration scenarios enable setting or changing options after all
IConfigureOptions<TOptions> configuration occurs.

IOptionsFactory<TOptions> is responsible for creating new options instances. It has a


single Create method. The default implementation takes all registered
IConfigureOptions<TOptions> and IPostConfigureOptions<TOptions> and runs all the
configurations first, followed by the post-configuration. It distinguishes between
IConfigureNamedOptions<TOptions> and IConfigureOptions<TOptions> and only calls
the appropriate interface.

IOptionsMonitorCache<TOptions> is used by IOptionsMonitor<TOptions> to cache


TOptions instances. The IOptionsMonitorCache<TOptions> invalidates options

instances in the monitor so that the value is recomputed (TryRemove). Values can be
manually introduced with TryAdd. The Clear method is used when all named instances
should be recreated on demand.

Use IOptionsSnapshot to read updated data


Using IOptionsSnapshot<TOptions>:

Options are computed once per request when accessed and cached for the lifetime
of the request.
May incur a significant performance penalty because it's a Scoped service and is
recomputed per request. For more information, see this GitHub issue and
Improve the performance of configuration binding .
Changes to the configuration are read after the app starts when using
configuration providers that support reading updated configuration values.

The difference between IOptionsMonitor and IOptionsSnapshot is that:

IOptionsMonitor is a Singleton service that retrieves current option values at any


time, which is especially useful in singleton dependencies.
IOptionsSnapshot is a Scoped service and provides a snapshot of the options at
the time the IOptionsSnapshot<T> object is constructed. Options snapshots are
designed for use with transient and scoped dependencies.

The following code uses IOptionsSnapshot<TOptions>.

C#

public class TestSnapModel : PageModel


{
private readonly MyOptions _snapshotOptions;
public TestSnapModel(IOptionsSnapshot<MyOptions>
snapshotOptionsAccessor)
{
_snapshotOptions = snapshotOptionsAccessor.Value;
}

public ContentResult OnGet()


{
return Content($"Option1: {_snapshotOptions.Option1} \n" +
$"Option2: {_snapshotOptions.Option2}");
}
}

The following code registers a configuration instance which MyOptions binds against:

C#

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

In the preceding code, changes to the JSON configuration file after the app has started
are read.

IOptionsMonitor
The following code registers a configuration instance which MyOptions binds against.

C#

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();


The following example uses IOptionsMonitor<TOptions>:

C#

public class TestMonitorModel : PageModel


{
private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )


{
_optionsDelegate = optionsDelegate;
}

public ContentResult OnGet()


{
return Content($"Option1: {_optionsDelegate.CurrentValue.Option1}
\n" +
$"Option2: {_optionsDelegate.CurrentValue.Option2}");
}
}

In the preceding code, by default, changes to the JSON configuration file after the app
has started are read.

Named options support using


IConfigureNamedOptions
Named options:

Are useful when multiple configuration sections bind to the same properties.
Are case sensitive.

Consider the following appsettings.json file:

JSON

{
"TopItem": {
"Month": {
"Name": "Green Widget",
"Model": "GW46"
},
"Year": {
"Name": "Orange Gadget",
"Model": "OG35"
}
}
}
Rather than creating two classes to bind TopItem:Month and TopItem:Year , the following
class is used for each section:

C#

public class TopItemSettings


{
public const string Month = "Month";
public const string Year = "Year";

public string Name { get; set; } = string.Empty;


public string Model { get; set; } = string.Empty;
}

The following code configures the named options:

C#

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

The following code displays the named options:

C#

public class TestNOModel : PageModel


{
private readonly TopItemSettings _monthTopItem;
private readonly TopItemSettings _yearTopItem;

public TestNOModel(IOptionsSnapshot<TopItemSettings>
namedOptionsAccessor)
{
_monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
_yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
}

public ContentResult OnGet()


{
return Content($"Month:Name {_monthTopItem.Name} \n" +
$"Month:Model {_monthTopItem.Model} \n\n" +
$"Year:Name {_yearTopItem.Name} \n" +
$"Year:Model {_yearTopItem.Model} \n" );
}
}

All options are named instances. IConfigureOptions<TOptions> instances are treated as


targeting the Options.DefaultName instance, which is string.Empty .
IConfigureNamedOptions<TOptions> also implements IConfigureOptions<TOptions>.
The default implementation of the IOptionsFactory<TOptions> has logic to use each
appropriately. The null named option is used to target all of the named instances
instead of a specific named instance. ConfigureAll and PostConfigureAll use this
convention.

OptionsBuilder API
OptionsBuilder<TOptions> is used to configure TOptions instances. OptionsBuilder
streamlines creating named options as it's only a single parameter to the initial
AddOptions<TOptions>(string optionsName) call instead of appearing in all of the
subsequent calls. Options validation and the ConfigureOptions overloads that accept
service dependencies are only available via OptionsBuilder .

OptionsBuilder is used in the Options validation section.

See Use AddOptions to configure custom repository for information adding a custom
repository.

Use DI services to configure options


Services can be accessed from dependency injection while configuring options in two
ways:

Pass a configuration delegate to Configure on OptionsBuilder<TOptions>.


OptionsBuilder<TOptions> provides overloads of Configure that allow use of up to
five services to configure options:

C#

builder.Services.AddOptions<MyOptions>("optionalName")
.Configure<Service1, Service2, Service3, Service4, Service5>(
(o, s, s2, s3, s4, s5) =>
o.Property = DoSomethingWith(s, s2, s3, s4, s5));
Create a type that implements IConfigureOptions<TOptions> or
IConfigureNamedOptions<TOptions> and register the type as a service.

We recommend passing a configuration delegate to Configure, since creating a service


is more complex. Creating a type is equivalent to what the framework does when calling
Configure. Calling Configure registers a transient generic
IConfigureNamedOptions<TOptions>, which has a constructor that accepts the generic
service types specified.

Options validation
Options validation enables option values to be validated.

Consider the following appsettings.json file:

JSON

{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}

The following class is used to bind to the "MyConfig" configuration section and applies a
couple of DataAnnotations rules:

C#

public class MyConfigOptions


{
public const string MyConfig = "MyConfig";

[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public string Key1 { get; set; }
[Range(0, 1000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int Key2 { get; set; }
public int Key3 { get; set; }
}

The following code:

Calls AddOptions to get an OptionsBuilder<TOptions> that binds to the


MyConfigOptions class.
Calls ValidateDataAnnotations to enable validation using DataAnnotations .

C#

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()

.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();

var app = builder.Build();

The ValidateDataAnnotations extension method is defined in the


Microsoft.Extensions.Options.DataAnnotations NuGet package. For web apps that use
the Microsoft.NET.Sdk.Web SDK, this package is referenced implicitly from the shared
framework.

The following code displays the configuration values or the validation errors:

C#

public class HomeController : Controller


{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyConfigOptions> _config;

public HomeController(IOptions<MyConfigOptions> config,


ILogger<HomeController> logger)
{
_config = config;
_logger = logger;

try
{
var configValue = _config.Value;

}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
_logger.LogError(failure);
}
}
}
public ContentResult Index()
{
string msg;
try
{
msg = $"Key1: {_config.Value.Key1} \n" +
$"Key2: {_config.Value.Key2} \n" +
$"Key3: {_config.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(msg);
}

The following code applies a more complex validation rule using a delegate:

C#

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()

.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Key2 != 0)
{
return config.Key3 > config.Key2;
}

return true;
}, "Key3 must be > than Key2."); // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> and IValidatableObject

The following class implements IValidateOptions<TOptions>:

C#

public class MyConfigValidation : IValidateOptions<MyConfigOptions>


{
public MyConfigOptions _config { get; private set; }

public MyConfigValidation(IConfiguration config)


{
_config = config.GetSection(MyConfigOptions.MyConfig)
.Get<MyConfigOptions>();
}

public ValidateOptionsResult Validate(string name, MyConfigOptions


options)
{
string? vor = null;
var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
var match = rx.Match(options.Key1!);

if (string.IsNullOrEmpty(match.Value))
{
vor = $"{options.Key1} doesn't match RegEx \n";
}

if ( options.Key2 < 0 || options.Key2 > 1000)


{
vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
}

if (_config.Key2 != default)
{
if(_config.Key3 <= _config.Key2)
{
vor += "Key3 must be > than Key2.";
}
}

if (vor != null)
{
return ValidateOptionsResult.Fail(vor);
}

return ValidateOptionsResult.Success;
}
}

IValidateOptions enables moving the validation code out of Program.cs and into a

class.

Using the preceding code, validation is enabled in Program.cs with the following code:

C#

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>
(builder.Configuration.GetSection(
MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
<MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

Options validation also supports IValidatableObject. To perform class-level validation of


a class within the class itself:

Implement the IValidatableObject interface and its Validate method within the
class.
Call ValidateDataAnnotations in Program.cs .

ValidateOnStart

Options validation runs the first time an IOptions<TOptions>,


IOptionsSnapshot<TOptions>, or IOptionsMonitor<TOptions> implementation is
created. To run options validation eagerly, when the app starts, call ValidateOnStart in
Program.cs :

C#

builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.ValidateOnStart();

Options post-configuration
Set post-configuration with IPostConfigureOptions<TOptions>. Post-configuration runs
after all IConfigureOptions<TOptions> configuration occurs:

C#

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()

.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure is available to post-configure named options:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>


{
myOptions.Name = "post_configured_name_value";
myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

Use PostConfigureAll to post-configure all configuration instances:

C#

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()

.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
Access options in Program.cs
To access IOptions<TOptions> or IOptionsMonitor<TOptions> in Program.cs , call
GetRequiredService on WebApplication.Services:

C#

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()


.CurrentValue.Option1;

Additional resources
View or download sample code (how to download)
Use multiple environments in ASP.NET
Core
Article • 04/11/2023

By Rick Anderson and Kirk Larkin

ASP.NET Core configures app behavior based on the runtime environment using an
environment variable.

Environments
To determine the runtime environment, ASP.NET Core reads from the following
environment variables:

1. DOTNET_ENVIRONMENT
2. ASPNETCORE_ENVIRONMENT when the WebApplication.CreateBuilder method is called.
The default ASP.NET Core web app templates call WebApplication.CreateBuilder .
The DOTNET_ENVIRONMENT value overrides ASPNETCORE_ENVIRONMENT when
WebApplicationBuilder is used. For other hosts, such as ConfigureWebHostDefaults
and WebHost.CreateDefaultBuilder , ASPNETCORE_ENVIRONMENT has higher
precedence.

IHostEnvironment.EnvironmentName can be set to any value, but the following values are

provided by the framework:

Development: The launchSettings.json file sets ASPNETCORE_ENVIRONMENT to


Development on the local machine.

Staging
Production: The default if DOTNET_ENVIRONMENT and ASPNETCORE_ENVIRONMENT have
not been set.

The following code:

Is similar to the code generated by the ASP.NET Core templates.


Enables the Developer Exception Page when ASPNETCORE_ENVIRONMENT is set to
Development . This is done automatically by the WebApplication.CreateBuilder

method.
Calls UseExceptionHandler when the value of ASPNETCORE_ENVIRONMENT is anything
other than Development .
Provides an IWebHostEnvironment instance in the Environment property of
WebApplication .

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 Environment Tag Helper uses the value of IHostEnvironment.EnvironmentName to


include or exclude markup in the element:

CSHTML

<environment include="Development">
<div>Environment is Development</div>
</environment>
<environment exclude="Development">
<div>Environment is NOT Development</div>
</environment>
<environment include="Staging,Development,Staging_2">
<div>Environment is: Staging, Development or Staging_2</div>
</environment>

The About page from the sample code includes the preceding markup and displays
the value of IWebHostEnvironment.EnvironmentName .
On Windows and macOS, environment variables and values aren't case-sensitive. Linux
environment variables and values are case-sensitive by default.

Create EnvironmentsSample
The sample code used in this article is based on a Razor Pages project named
EnvironmentsSample.

The following .NET CLI commands create and run a web app named
EnvironmentsSample:

Bash

dotnet new webapp -o EnvironmentsSample


cd EnvironmentsSample
dotnet run --verbosity normal

When the app runs, it displays output similar to the following:

Bash

info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7152
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5105
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:\Path\To\EnvironmentsSample

Set environment on the command line


Use the --environment flag to set the environment. For example:

.NET CLI

dotnet run --environment Production

The preceding command sets the environment to Production and displays output
similar to the following in the command window:

Bash
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7262
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5005
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Path\To\EnvironmentsSample

Development and launchSettings.json


The development environment can enable features that shouldn't be exposed in
production. For example, the ASP.NET Core project templates enable the Developer
Exception Page in the development environment. Because of the performance cost,
scope validation and dependency validation only happens in development.

The environment for local machine development can be set in the


Properties\launchSettings.json file of the project. Environment values set in
launchSettings.json override values set in the system environment.

The launchSettings.json file:

Is only used on the local development machine.


Is not deployed.
Contains profile settings.

The following JSON shows the launchSettings.json file for an ASP.NET Core web
project named EnvironmentsSample created with Visual Studio or dotnet new :

JSON

{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:59481",
"sslPort": 44308
}
},
"profiles": {
"EnvironmentsSample": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7152;http://localhost:5105",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

The preceding JSON contains two profiles:

EnvironmentsSample : The profile name is the project name. As the first profile listed,

this profile is used by default. The "commandName" key has the value "Project" ,
therefore, the Kestrel web server is launched.

IIS Express : The "commandName" key has the value "IISExpress" , therefore,

IISExpress is the web server.

You can set the launch profile to the project or any other profile included in
launchSettings.json . For example, in the image below, selecting the project name

launches the Kestrel web server.

The value of commandName can specify the web server to launch. commandName can be any
one of the following:
IISExpress : Launches IIS Express.

IIS : No web server launched. IIS is expected to be available.


Project : Launches Kestrel.

The Visual Studio 2022 project properties Debug / General tab provides an Open debug
launch profiles UI link. This link opens a Launch Profiles dialog that lets you edit the
environment variable settings in the launchSettings.json file. You can also open the
Launch Profiles dialog from the Debug menu by selecting <project name> Debug
Properties. Changes made to project profiles may not take effect until the web server is
restarted. Kestrel must be restarted before it can detect changes made to its
environment.

The following launchSettings.json file contains multiple profiles:

JSON

{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:59481",
"sslPort": 44308
}
},
"profiles": {
"EnvironmentsSample": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7152;http://localhost:5105",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"EnvironmentsSample-Staging": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7152;http://localhost:5105",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Staging",
"ASPNETCORE_DETAILEDERRORS": "1",
"ASPNETCORE_SHUTDOWNTIMEOUTSECONDS": "3"
}
},
"EnvironmentsSample-Production": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7152;http://localhost:5105",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

Profiles can be selected:

From the Visual Studio UI.

Using the dotnet run CLI command with the --launch-profile option set to the
profile's name. This approach only supports Kestrel profiles.

.NET CLI

dotnet run --launch-profile "SampleApp"

2 Warning
launchSettings.json shouldn't store secrets. The Secret Manager tool can be used

to store secrets for local development.

When using Visual Studio Code , environment variables can be set in the
.vscode/launch.json file. The following example sets several environment variables for
Host configuration values:

JSON

{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
// Configuration ommitted for brevity.
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:5001",
"ASPNETCORE_DETAILEDERRORS": "1",
"ASPNETCORE_SHUTDOWNTIMEOUTSECONDS": "3"
},
// Configuration ommitted for brevity.

The .vscode/launch.json file is used only by Visual Studio Code.

Production
The production environment should be configured to maximize security, performance,
and application robustness. Some common settings that differ from development
include:

Caching.
Client-side resources are bundled, minified, and potentially served from a CDN.
Diagnostic error pages disabled.
Friendly error pages enabled.
Production logging and monitoring enabled. For example, using Application
Insights.

Set the environment by setting an environment


variable
It's often useful to set a specific environment for testing with an environment variable or
platform setting. If the environment isn't set, it defaults to Production , which disables
most debugging features. The method for setting the environment depends on the
operating system.

When the host is built, the last environment setting read by the app determines the
app's environment. The app's environment can't be changed while the app is running.

The About page from the sample code displays the value of
IWebHostEnvironment.EnvironmentName .

Azure App Service


Production is the default value if DOTNET_ENVIRONMENT and ASPNETCORE_ENVIRONMENT have
not been set. Apps deployed to Azure are Production by default.

To set the environment in an Azure App Service app by using the portal:

1. Select the app from the App Services page.


2. In the Settings group, select Configuration.
3. In the Application settings tab, select New application setting.
4. In the Add/Edit application setting window, provide ASPNETCORE_ENVIRONMENT for
the Name. For Value, provide the environment (for example, Staging ).
5. Select the Deployment slot setting checkbox if you wish the environment setting
to remain with the current slot when deployment slots are swapped. For more
information, see Set up staging environments in Azure App Service in the Azure
documentation.
6. Select OK to close the Add/Edit application setting dialog.
7. Select Save at the top of the Configuration page.

Azure App Service automatically restarts the app after an app setting is added, changed,
or deleted in the Azure portal.

Windows - Set environment variable for a process


Environment values in launchSettings.json override values set in the system
environment.

To set the ASPNETCORE_ENVIRONMENT for the current session when the app is started using
dotnet run, use the following commands at a command prompt or in PowerShell:

Console
set ASPNETCORE_ENVIRONMENT=Staging
dotnet run --no-launch-profile

PowerShell

$Env:ASPNETCORE_ENVIRONMENT = "Staging"
dotnet run --no-launch-profile

Windows - Set environment variable globally


The preceding commands set ASPNETCORE_ENVIRONMENT only for processes launched from
that command window.

To set the value globally in Windows, use either of the following approaches:

Open the Control Panel > System > Advanced system settings and add or edit
the ASPNETCORE_ENVIRONMENT value:

Open an administrative command prompt and use the setx command or open an
administrative PowerShell command prompt and use
[Environment]::SetEnvironmentVariable :
Console

setx ASPNETCORE_ENVIRONMENT Staging /M

The /M switch sets the environment variable at the system level. If the /M switch
isn't used, the environment variable is set for the user account.

PowerShell

[Environment]::SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT",
"Staging", "Machine")

The Machine option sets the environment variable at the system level. If the
option value is changed to User , the environment variable is set for the user
account.

When the ASPNETCORE_ENVIRONMENT environment variable is set globally, it takes effect for
dotnet run in any command window opened after the value is set. Environment values
in launchSettings.json override values set in the system environment.

Windows - Use web.config


To set the ASPNETCORE_ENVIRONMENT environment variable with web.config , see the Set
environment variables section of web.config file.

Windows - IIS deployments


Include the <EnvironmentName> property in the publish profile (.pubxml) or project file.
This approach sets the environment in web.config when the project is published:

XML

<PropertyGroup>
<EnvironmentName>Development</EnvironmentName>
</PropertyGroup>

To set the ASPNETCORE_ENVIRONMENT environment variable for an app running in an


isolated Application Pool (supported on IIS 10.0 or later), see the AppCmd.exe command
section of Environment Variables <environmentVariables>. When the
ASPNETCORE_ENVIRONMENT environment variable is set for an app pool, its value overrides
a setting at the system level.
When hosting an app in IIS and adding or changing the ASPNETCORE_ENVIRONMENT
environment variable, use one of the following approaches to have the new value picked
up by apps:

Execute net stop was /y followed by net start w3svc from a command prompt.
Restart the server.

macOS
Setting the current environment for macOS can be performed in-line when running the
app:

Bash

ASPNETCORE_ENVIRONMENT=Staging dotnet run

Alternatively, set the environment with export prior to running the app:

Bash

export ASPNETCORE_ENVIRONMENT=Staging

Machine-level environment variables are set in the .bashrc or .bash_profile file. Edit the
file using any text editor. Add the following statement:

Bash

export ASPNETCORE_ENVIRONMENT=Staging

Linux
For Linux distributions, use the export command at a command prompt for session-
based variable settings and the bash_profile file for machine-level environment settings.

Set the environment in code


To set the environment in code, use WebApplicationOptions.EnvironmentName when
creating WebApplicationBuilder, as shown in the following example:

C#
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = Environments.Staging
});

// 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();

For more information, see .NET Generic Host in ASP.NET Core.

Configuration by environment
To load configuration by environment, see Configuration in ASP.NET Core.

Configure services and middleware by


environment
Use WebApplicationBuilder.Environment or WebApplication.Environment to
conditionally add services or middleware depending on the current environment. The
project template includes an example of code that adds middleware only when the
current environment isn't Development:

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 highlighted code checks the current environment while building the request
pipeline. To check the current environment while configuring services, use
builder.Environment instead of app.Environment .

Additional resources
View or download sample code (how to download)
App startup in ASP.NET Core
Configuration in ASP.NET Core
ASP.NET Core Blazor environments
Logging in .NET Core and ASP.NET Core
Article • 03/17/2023

By Kirk Larkin , Juergen Gutsch , and Rick Anderson

This topic describes logging in .NET as it applies to ASP.NET Core apps. For detailed
information on logging in .NET, see Logging in .NET. For more information on logging in
Blazor apps, see ASP.NET Core Blazor logging.

Logging providers
Logging providers store logs, except for the Console provider which displays logs. For
example, the Azure Application Insights provider stores logs in Azure Application
Insights. Multiple providers can be enabled.

The default ASP.NET Core web app templates:

Use the Generic Host.


Call WebApplication.CreateBuilder, which adds the following logging providers:
Console
Debug
EventSource
EventLog: Windows only

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
app.Run();

The preceding code shows the Program.cs file created with the ASP.NET Core web app
templates. The next several sections provide samples based on the ASP.NET Core web
app templates, which use the Generic Host.

The following code overrides the default set of logging providers added by
WebApplication.CreateBuilder :

C#

var builder = WebApplication.CreateBuilder(args);


builder.Logging.ClearProviders();
builder.Logging.AddConsole();

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Alternatively, the preceding logging code can be written as follows:

C#

var builder = WebApplication.CreateBuilder();


builder.Host.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
});

For additional providers, see:


Built-in logging providers
Third-party logging providers.

Create logs
To create logs, use an ILogger<TCategoryName> object from dependency injection (DI).

The following example:

Creates a logger, ILogger<AboutModel> , which uses a log category of the fully


qualified name of the type AboutModel . The log category is a string that is
associated with each log.
Calls LogInformation to log at the Information level. The Log level indicates the
severity of the logged event.

C#

public class AboutModel : PageModel


{
private readonly ILogger _logger;

public AboutModel(ILogger<AboutModel> logger)


{
_logger = logger;
}

public void OnGet()


{
_logger.LogInformation("About page visited at {DT}",
DateTime.UtcNow.ToLongTimeString());
}
}

Levels and categories are explained in more detail later in this document.

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

Configure logging
Logging configuration is commonly provided by the Logging section of appsettings.
{ENVIRONMENT}.json files, where the {ENVIRONMENT} placeholder is the environment. The
following appsettings.Development.json file is generated by the ASP.NET Core web app
templates:

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

In the preceding JSON:

The "Default" and "Microsoft.AspNetCore" categories are specified.


The "Microsoft.AspNetCore" category applies to all categories that start with
"Microsoft.AspNetCore" . For example, this setting applies to the

"Microsoft.AspNetCore.Routing.EndpointMiddleware" category.
The "Microsoft.AspNetCore" category logs at log level Warning and higher.
A specific log provider is not specified, so LogLevel applies to all the enabled
logging providers except for the Windows EventLog.

The Logging property can have LogLevel and log provider properties. The LogLevel
specifies the minimum level to log for selected categories. In the preceding JSON,
Information and Warning log levels are specified. LogLevel indicates the severity of the

log and ranges from 0 to 6:

Trace = 0, Debug = 1, Information = 2, Warning = 3, Error = 4, Critical = 5, and None


= 6.

When a LogLevel is specified, logging is enabled for messages at the specified level and
higher. In the preceding JSON, the Default category is logged for Information and
higher. For example, Information , Warning , Error , and Critical messages are logged.
If no LogLevel is specified, logging defaults to the Information level. For more
information, see Log levels.

A provider property can specify a LogLevel property. LogLevel under a provider


specifies levels to log for that provider, and overrides the non-provider log settings.
Consider the following appsettings.json file:

JSON

{
"Logging": {
"LogLevel": { // All providers, LogLevel applies to all the enabled
providers.
"Default": "Error", // Default logging, Error and higher.
"Microsoft": "Warning" // All Microsoft* categories, Warning and
higher.
},
"Debug": { // Debug provider.
"LogLevel": {
"Default": "Information", // Overrides preceding LogLevel:Default
setting.
"Microsoft.Hosting": "Trace" // Debug:Microsoft.Hosting category.
}
},
"EventSource": { // EventSource provider
"LogLevel": {
"Default": "Warning" // All categories of EventSource provider.
}
}
}
}

Settings in Logging.{PROVIDER NAME}.LogLevel override settings in Logging.LogLevel ,


where the {PROVIDER NAME} placeholder is the provider name. In the preceding JSON,
the Debug provider's default log level is set to Information :

Logging:Debug:LogLevel:Default:Information

The preceding setting specifies the Information log level for every Logging:Debug:
category except Microsoft.Hosting . When a specific category is listed, the specific
category overrides the default category. In the preceding JSON, the
Logging:Debug:LogLevel categories "Microsoft.Hosting" and "Default" override the
settings in Logging:LogLevel .

The minimum log level can be specified for any of:

Specific providers: For example,


Logging:EventSource:LogLevel:Default:Information

Specific categories: For example, Logging:LogLevel:Microsoft:Warning


All providers and all categories: Logging:LogLevel:Default:Warning

Any logs below the minimum level are not:

Passed to the provider.


Logged or displayed.

To suppress all logs, specify LogLevel.None. LogLevel.None has a value of 6, which is


higher than LogLevel.Critical (5).
If a provider supports log scopes, IncludeScopes indicates whether they're enabled. For
more information, see log scopes.

The following appsettings.json file contains all the providers enabled by default:

JSON

{
"Logging": {
"LogLevel": { // No provider, LogLevel applies to all the enabled
providers.
"Default": "Error",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Warning"
},
"Debug": { // Debug provider.
"LogLevel": {
"Default": "Information" // Overrides preceding LogLevel:Default
setting.
}
},
"Console": {
"IncludeScopes": true,
"LogLevel": {
"Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
"Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
"Microsoft.AspNetCore.Mvc.Razor": "Error",
"Default": "Information"
}
},
"EventSource": {
"LogLevel": {
"Microsoft": "Information"
}
},
"EventLog": {
"LogLevel": {
"Microsoft": "Information"
}
},
"AzureAppServicesFile": {
"IncludeScopes": true,
"LogLevel": {
"Default": "Warning"
}
},
"AzureAppServicesBlob": {
"IncludeScopes": true,
"LogLevel": {
"Microsoft": "Information"
}
},
"ApplicationInsights": {
"LogLevel": {
"Default": "Information"
}
}
}
}

In the preceding sample:

The categories and levels are not suggested values. The sample is provided to
show all the default providers.
Settings in Logging.{PROVIDER NAME}.LogLevel override settings in
Logging.LogLevel , where the {PROVIDER NAME} placeholder is the provider name.

For example, the level in Debug.LogLevel.Default overrides the level in


LogLevel.Default .

Each default provider alias is used. Each provider defines an alias that can be used
in configuration in place of the fully qualified type name. The built-in providers
aliases are:
Console
Debug

EventSource

EventLog
AzureAppServicesFile

AzureAppServicesBlob
ApplicationInsights

Log in Program.cs
The following example calls Builder.WebApplication.Logger in Program.cs and logs
informational messages:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();
app.Logger.LogInformation("Adding Routes");
app.MapGet("/", () => "Hello World!");
app.Logger.LogInformation("Starting the app");
app.Run();

The following example calls AddConsole in Program.cs and logs the /Test endpoint:

C#
var builder = WebApplication.CreateBuilder(args);

var logger = LoggerFactory.Create(config =>


{
config.AddConsole();
}).CreateLogger("Program");

var app = builder.Build();

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

app.MapGet("/Test", async context =>


{
logger.LogInformation("Testing logging in Program.cs");
await context.Response.WriteAsync("Testing");
});

app.Run();

The following example calls AddSimpleConsole in Program.cs , disables color output,


and logs the /Test endpoint:

C#

using Microsoft.Extensions.Logging.Console;

var builder = WebApplication.CreateBuilder(args);

using var loggerFactory = LoggerFactory.Create(builder =>


{
builder.AddSimpleConsole(i => i.ColorBehavior =
LoggerColorBehavior.Disabled);
});

var logger = loggerFactory.CreateLogger<Program>();

var app = builder.Build();

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

app.MapGet("/Test", async context =>


{
logger.LogInformation("Testing logging in Program.cs");
await context.Response.WriteAsync("Testing");
});

app.Run();
Set log level by command line, environment
variables, and other configuration
Log level can be set by any of the configuration providers.

The : separator doesn't work with environment variable hierarchical keys on all
platforms. __ , the double underscore, is:

Supported by all platforms. For example, the : separator is not supported by


Bash , but __ is.
Automatically replaced by a :

The following commands:

Set the environment key Logging:LogLevel:Microsoft to a value of Information on


Windows.
Test the settings when using an app created with the ASP.NET Core web
application templates. The dotnet run command must be run in the project
directory after using set .

.NET CLI

set Logging__LogLevel__Microsoft=Information
dotnet run

The preceding environment setting:

Is only set in processes launched from the command window they were set in.
Isn't read by browsers launched with Visual Studio.

The following setx command also sets the environment key and value on Windows.
Unlike set , setx settings are persisted. The /M switch sets the variable in the system
environment. If /M isn't used, a user environment variable is set.

Console

setx Logging__LogLevel__Microsoft Information /M

Consider the following appsettings.json file:

JSON
"Logging": {
"Console": {
"LogLevel": {
"Microsoft.Hosting.Lifetime": "Trace"
}
}
}

The following command sets the preceding configuration in the environment:

Console

setx Logging__Console__LogLevel__Microsoft.Hosting.Lifetime Trace /M

On Azure App Service , select New application setting on the Settings >
Configuration page. Azure App Service application settings are:

Encrypted at rest and transmitted over an encrypted channel.


Exposed as environment variables.

For more information, see Azure Apps: Override app configuration using the Azure
Portal.

For more information on setting ASP.NET Core configuration values using environment
variables, see environment variables. For information on using other configuration
sources, including the command line, Azure Key Vault, Azure App Configuration, other
file formats, and more, see Configuration in ASP.NET Core.

How filtering rules are applied


When an ILogger<TCategoryName> object is created, the ILoggerFactory object selects
a single rule per provider to apply to that logger. All messages written by an ILogger
instance are filtered based on the selected rules. The most specific rule for each provider
and category pair is selected from the available rules.

The following algorithm is used for each provider when an ILogger is created for a
given category:

Select all rules that match the provider or its alias. If no match is found, select all
rules with an empty provider.
From the result of the preceding step, select rules with longest matching category
prefix. If no match is found, select all rules that don't specify a category.
If multiple rules are selected, take the last one.
If no rules are selected, use MinimumLevel .

Logging output from dotnet run and Visual


Studio
Logs created with the default logging providers are displayed:

In Visual Studio
In the Debug output window when debugging.
In the ASP.NET Core Web Server window.
In the console window when the app is run with dotnet run .

Logs that begin with "Microsoft" categories are from ASP.NET Core framework code.
ASP.NET Core and application code use the same logging API and providers.

Log category
When an ILogger object is created, a category is specified. That category is included
with each log message created by that instance of ILogger . The category string is
arbitrary, but the convention is to use the class name. For example, in a controller the
name might be "TodoApi.Controllers.TodoController" . The ASP.NET Core web apps use
ILogger<T> to automatically get an ILogger instance that uses the fully qualified type
name of T as the category:

C#

public class PrivacyModel : PageModel


{
private readonly ILogger<PrivacyModel> _logger;

public PrivacyModel(ILogger<PrivacyModel> logger)


{
_logger = logger;
}

public void OnGet()


{
_logger.LogInformation("GET Pages.PrivacyModel called.");
}
}

To explicitly specify the category, call ILoggerFactory.CreateLogger :


C#

public class ContactModel : PageModel


{
private readonly ILogger _logger;

public ContactModel(ILoggerFactory logger)


{
_logger = logger.CreateLogger("MyCategory");
}

public void OnGet()


{
_logger.LogInformation("GET Pages.ContactModel called.");
}

Calling CreateLogger with a fixed name can be useful when used in multiple methods so
the events can be organized by category.

ILogger<T> is equivalent to calling CreateLogger with the fully qualified type name of T .

Log level
The following table lists the LogLevel values, the convenience Log{LogLevel} extension
method, and the suggested usage:

LogLevel Value Method Description

Trace 0 LogTrace Contain the most detailed messages. These messages


may contain sensitive app data. These messages are
disabled by default and should not be enabled in
production.

Debug 1 LogDebug For debugging and development. Use with caution in


production due to the high volume.

Information 2 LogInformation Tracks the general flow of the app. May have long-term
value.

Warning 3 LogWarning For abnormal or unexpected events. Typically includes


errors or conditions that don't cause the app to fail.

Error 4 LogError For errors and exceptions that cannot be handled. These
messages indicate a failure in the current operation or
request, not an app-wide failure.

Critical 5 LogCritical For failures that require immediate attention. Examples:


data loss scenarios, out of disk space.
LogLevel Value Method Description

None 6 Specifies that a logging category shouldn't write


messages.

In the previous table, the LogLevel is listed from lowest to highest severity.

The Log method's first parameter, LogLevel, indicates the severity of the log. Rather than
calling Log(LogLevel, ...) , most developers call the Log{LOG LEVEL} extension
methods, where the {LOG LEVEL} placeholder is the log level. For example, the following
two logging calls are functionally equivalent and produce the same log:

C#

[HttpGet]
public IActionResult Test1(int id)
{
var routeInfo = ControllerContext.ToCtxString(id);

_logger.Log(LogLevel.Information, MyLogEvents.TestItem, routeInfo);


_logger.LogInformation(MyLogEvents.TestItem, routeInfo);

return ControllerContext.MyDisplayRouteInfo();
}

MyLogEvents.TestItem is the event ID. MyLogEvents is part of the sample app and is
displayed in the Log event ID section.

MyDisplayRouteInfo and ToCtxString are provided by the


Rick.Docs.Samples.RouteInfo NuGet package. The methods display Controller and
Razor Page route information.

The following code creates Information and Warning logs:

C#

[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
_logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", id);

var todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null)
{
_logger.LogWarning(MyLogEvents.GetItemNotFound, "Get({Id}) NOT
FOUND", id);
return NotFound();
}

return ItemToDTO(todoItem);
}

In the preceding code, the first Log{LOG LEVEL} parameter, MyLogEvents.GetItem , is the
Log event ID. The second parameter is a message template with placeholders for
argument values provided by the remaining method parameters. The method
parameters are explained in the message template section later in this document.

Call the appropriate Log{LOG LEVEL} method to control how much log output is written
to a particular storage medium. For example:

In production:
Logging at the Trace or Information levels produces a high-volume of detailed
log messages. To control costs and not exceed data storage limits, log Trace
and Information level messages to a high-volume, low-cost data store.
Consider limiting Trace and Information to specific categories.
Logging at Warning through Critical levels should produce few log messages.
Costs and storage limits usually aren't a concern.
Few logs allow more flexibility in data store choices.
In development:
Set to Warning .
Add Trace or Information messages when troubleshooting. To limit output, set
Trace or Information only for the categories under investigation.

ASP.NET Core writes logs for framework events. For example, consider the log output
for:

A Razor Pages app created with the ASP.NET Core templates.


Logging set to Logging:Console:LogLevel:Microsoft:Information .
Navigation to the Privacy page:

Console

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/2 GET https://localhost:5001/Privacy
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint '/Privacy'
info:
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[3]
Route matched with {page = "/Privacy"}. Executing page /Privacy
info:
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[101]
Executing handler method DefaultRP.Pages.PrivacyModel.OnGet -
ModelState is Valid
info:
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[102]
Executed handler method OnGet, returned result .
info:
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[103]
Executing an implicit handler method - ModelState is Valid
info:
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[104]
Executed an implicit handler method, returned result
Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
info:
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[4]
Executed page /Privacy in 74.5188ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint '/Privacy'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 149.3023ms 200 text/html; charset=utf-8

The following JSON sets Logging:Console:LogLevel:Microsoft:Information :

JSON

{
"Logging": { // Default, all providers.
"LogLevel": {
"Microsoft": "Warning"
},
"Console": { // Console provider.
"LogLevel": {
"Microsoft": "Information"
}
}
}
}

Log event ID
Each log can specify an event ID. The sample app uses the MyLogEvents class to define
event IDs:

C#

public class MyLogEvents


{
public const int GenerateItems = 1000;
public const int ListItems = 1001;
public const int GetItem = 1002;
public const int InsertItem = 1003;
public const int UpdateItem = 1004;
public const int DeleteItem = 1005;

public const int TestItem = 3000;

public const int GetItemNotFound = 4000;


public const int UpdateItemNotFound = 4001;
}

C#

[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
_logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", id);

var todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null)
{
_logger.LogWarning(MyLogEvents.GetItemNotFound, "Get({Id}) NOT
FOUND", id);
return NotFound();
}

return ItemToDTO(todoItem);
}

An event ID associates a set of events. For example, all logs related to displaying a list of
items on a page might be 1001.

The logging provider may store the event ID in an ID field, in the logging message, or
not at all. The Debug provider doesn't show event IDs. The console provider shows
event IDs in brackets after the category:

Console

info: TodoApi.Controllers.TodoItemsController[1002]
Getting item 1
warn: TodoApi.Controllers.TodoItemsController[4000]
Get(1) NOT FOUND

Some logging providers store the event ID in a field, which allows for filtering on the ID.

Log message template


Each log API uses a message template. The message template can contain placeholders
for which arguments are provided. Use names for the placeholders, not numbers.

C#

[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
_logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", id);

var todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null)
{
_logger.LogWarning(MyLogEvents.GetItemNotFound, "Get({Id}) NOT
FOUND", id);
return NotFound();
}

return ItemToDTO(todoItem);
}

The order of the parameters, not their placeholder names, determines which parameters
are used to provide placeholder values in log messages. In the following code, the
parameter names are out of sequence in the placeholders of the message template:

C#

string apples = 1;
string pears = 2;
string bananas = 3;

_logger.LogInformation("Parameters: {pears}, {bananas}, {apples}", apples,


pears, bananas);

However, the parameters are assigned to the placeholders in the order: apples , pears ,
bananas . The log message reflects the order of the parameters:

text

Parameters: 1, 2, 3

This approach allows logging providers to implement semantic or structured logging .


The arguments themselves are passed to the logging system, not just the formatted
message template. This enables logging providers to store the parameter values as
fields. For example, consider the following logger method:
C#

_logger.LogInformation("Getting item {Id} at {RequestTime}", id,


DateTime.Now);

For example, when logging to Azure Table Storage:

Each Azure Table entity can have ID and RequestTime properties.


Tables with properties simplify queries on logged data. For example, a query can
find all logs within a particular RequestTime range without having to parse the time
out of the text message.

Log exceptions
The logger methods have overloads that take an exception parameter:

C#

[HttpGet("{id}")]
public IActionResult TestExp(int id)
{
var routeInfo = ControllerContext.ToCtxString(id);
_logger.LogInformation(MyLogEvents.TestItem, routeInfo);

try
{
if (id == 3)
{
throw new Exception("Test exception");
}
}
catch (Exception ex)
{
_logger.LogWarning(MyLogEvents.GetItemNotFound, ex, "TestExp({Id})",
id);
return NotFound();
}

return ControllerContext.MyDisplayRouteInfo();
}

MyDisplayRouteInfo and ToCtxString are provided by the


Rick.Docs.Samples.RouteInfo NuGet package. The methods display Controller and
Razor Page route information.

Exception logging is provider-specific.


Default log level
If the default log level is not set, the default log level value is Information .

For example, consider the following web app:

Created with the ASP.NET web app templates.


appsettings.json and appsettings.Development.json deleted or renamed.

With the preceding setup, navigating to the privacy or home page produces many
Trace , Debug , and Information messages with Microsoft in the category name.

The following code sets the default log level when the default log level is not set in
configuration:

C#

var builder = WebApplication.CreateBuilder();


builder.Logging.SetMinimumLevel(LogLevel.Warning);

Generally, log levels should be specified in configuration and not code.

Filter function
A filter function is invoked for all providers and categories that don't have rules assigned
to them by configuration or code:

C#

var builder = WebApplication.CreateBuilder();


builder.Logging.AddFilter((provider, category, logLevel) =>
{
if (provider.Contains("ConsoleLoggerProvider")
&& category.Contains("Controller")
&& logLevel >= LogLevel.Information)
{
return true;
}
else if (provider.Contains("ConsoleLoggerProvider")
&& category.Contains("Microsoft")
&& logLevel >= LogLevel.Information)
{
return true;
}
else
{
return false;
}
});

The preceding code displays console logs when the category contains Controller or
Microsoft and the log level is Information or higher.

Generally, log levels should be specified in configuration and not code.

ASP.NET Core and EF Core categories


The following table contains some categories used by ASP.NET Core and Entity
Framework Core, with notes about the logs:

Category Notes

Microsoft.AspNetCore General ASP.NET Core diagnostics.

Microsoft.AspNetCore.DataProtection Which keys were considered, found, and used.

Microsoft.AspNetCore.HostFiltering Hosts allowed.

Microsoft.AspNetCore.Hosting How long HTTP requests took to complete and what time
they started. Which hosting startup assemblies were
loaded.

Microsoft.AspNetCore.Mvc MVC and Razor diagnostics. Model binding, filter


execution, view compilation, action selection.

Microsoft.AspNetCore.Routing Route matching information.

Microsoft.AspNetCore.Server Connection start, stop, and keep alive responses. HTTPS


certificate information.

Microsoft.AspNetCore.StaticFiles Files served.

Microsoft.EntityFrameworkCore General Entity Framework Core diagnostics. Database


activity and configuration, change detection, migrations.

To view more categories in the console window, set appsettings.Development.json to


the following:

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Trace",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

Log scopes
A scope can group a set of logical operations. This grouping can be used to attach the
same data to each log that's created as part of a set. For example, every log created as
part of processing a transaction can include the transaction ID.

A scope:

Is an IDisposable type that's returned by the BeginScope method.


Lasts until it's disposed.

The following providers support scopes:

Console
AzureAppServicesFile and AzureAppServicesBlob

Use a scope by wrapping logger calls in a using block:

C#

[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
TodoItem todoItem;
var transactionId = Guid.NewGuid().ToString();
using (_logger.BeginScope(new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("TransactionId",
transactionId),
}))
{
_logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}",
id);

todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null)
{
_logger.LogWarning(MyLogEvents.GetItemNotFound,
"Get({Id}) NOT FOUND", id);
return NotFound();
}
}
return ItemToDTO(todoItem);
}

Built-in logging providers


ASP.NET Core includes the following logging providers as part of the shared framework:

Console
Debug
EventSource
EventLog

The following logging providers are shipped by Microsoft, but not as part of the shared
framework. They must be installed as additional nuget.

AzureAppServicesFile and AzureAppServicesBlob


ApplicationInsights

ASP.NET Core doesn't include a logging provider for writing logs to files. To write logs to
files from an ASP.NET Core app, consider using a third-party logging provider.

For information on stdout and debug logging with the ASP.NET Core Module, see
Troubleshoot ASP.NET Core on Azure App Service and IIS and ASP.NET Core Module
(ANCM) for IIS.

Console
The Console provider logs output to the console. For more information on viewing
Console logs in development, see Logging output from dotnet run and Visual Studio.

Debug
The Debug provider writes log output by using the System.Diagnostics.Debug class. Calls
to System.Diagnostics.Debug.WriteLine write to the Debug provider.

On Linux, the Debug provider log location is distribution-dependent and may be one of
the following:

/var/log/message

/var/log/syslog
Event Source
The EventSource provider writes to a cross-platform event source with the name
Microsoft-Extensions-Logging . On Windows, the provider uses ETW.

dotnet trace tooling

The dotnet-trace tool is a cross-platform CLI global tool that enables the collection of
.NET Core traces of a running process. The tool collects
Microsoft.Extensions.Logging.EventSource provider data using a LoggingEventSource.

For installation instructions, see dotnet-trace.

Use the dotnet trace tooling to collect a trace from an app:

1. Run the app with the dotnet run command.

2. Determine the process identifier (PID) of the .NET Core app:

.NET CLI

dotnet trace ps

Find the PID for the process that has the same name as the app's assembly.

3. Execute the dotnet trace command.

General command syntax:

.NET CLI

dotnet trace collect -p {PID}


--providers Microsoft-Extensions-Logging:{Keyword}:{Provider Level}
:FilterSpecs=\"
{Logger Category 1}:{Category Level 1};
{Logger Category 2}:{Category Level 2};
...
{Logger Category N}:{Category Level N}\"

When using a PowerShell command shell, enclose the --providers value in single
quotes ( ' ):

.NET CLI

dotnet trace collect -p {PID}


--providers 'Microsoft-Extensions-Logging:{Keyword}:{Provider
Level}
:FilterSpecs=\"
{Logger Category 1}:{Category Level 1};
{Logger Category 2}:{Category Level 2};
...
{Logger Category N}:{Category Level N}\"'

On non-Windows platforms, add the -f speedscope option to change the format


of the output trace file to speedscope .

The following table defines the Keyword:

Keyword Description

1 Log meta events about the LoggingEventSource . Doesn't log events from
ILogger .

2 Turns on the Message event when ILogger.Log() is called. Provides information


in a programmatic (not formatted) way.

4 Turns on the FormatMessage event when ILogger.Log() is called. Provides the


formatted string version of the information.

8 Turns on the MessageJson event when ILogger.Log() is called. Provides a JSON


representation of the arguments.

The following table lists the provider levels:

Provider Level Description

0 LogAlways

1 Critical

2 Error

3 Warning

4 Informational

5 Verbose

The parsing for a category level can be either a string or a number:

Category named value Numeric value

Trace 0

Debug 1
Category named value Numeric value

Information 2

Warning 3

Error 4

Critical 5

The provider level and category level:

Are in reverse order.


The string constants aren't all identical.

If no FilterSpecs are specified then the EventSourceLogger implementation


attempts to convert the provider level to a category level and applies it to all
categories.

Provider Level Category Level

Verbose (5) Debug (1)

Informational (4) Information (2)

Warning (3) Warning (3)

Error (2) Error (4)

Critical (1) Critical (5)

If FilterSpecs are provided, any category that is included in the list uses the
category level encoded there, all other categories are filtered out.

The following examples assume:

An app is running and calling logger.LogDebug("12345") .


The process ID (PID) has been set via set PID=12345 , where 12345 is the
actual PID.

Consider the following command:

.NET CLI

dotnet trace collect -p %PID% --providers Microsoft-Extensions-


Logging:4:5
The preceding command:

Captures debug messages.


Doesn't apply a FilterSpecs .
Specifies level 5 which maps category Debug.

Consider the following command:

.NET CLI

dotnet trace collect -p %PID% --providers Microsoft-Extensions-


Logging:4:5:\"FilterSpecs=*:5\"

The preceding command:

Doesn't capture debug messages because the category level 5 is Critical .


Provides a FilterSpecs .

The following command captures debug messages because category level 1


specifies Debug .

.NET CLI

dotnet trace collect -p %PID% --providers Microsoft-Extensions-


Logging:4:5:\"FilterSpecs=*:1\"

The following command captures debug messages because category specifies


Debug .

.NET CLI

dotnet trace collect -p %PID% --providers Microsoft-Extensions-


Logging:4:5:\"FilterSpecs=*:Debug\"

FilterSpecs entries for {Logger Category} and {Category Level} represent

additional log filtering conditions. Separate FilterSpecs entries with the ;


semicolon character.

Example using a Windows command shell:

.NET CLI

dotnet trace collect -p %PID% --providers Microsoft-Extensions-


Logging:4:2:FilterSpecs=\"Microsoft.AspNetCore.Hosting*:4\"
The preceding command activates:

The Event Source logger to produce formatted strings ( 4 ) for errors ( 2 ).


Microsoft.AspNetCore.Hosting logging at the Informational logging level

( 4 ).

4. Stop the dotnet trace tooling by pressing the Enter key or Ctrl + C .

The trace is saved with the name trace.nettrace in the folder where the dotnet
trace command is executed.

5. Open the trace with Perfview. Open the trace.nettrace file and explore the trace
events.

If the app doesn't build the host with WebApplication.CreateBuilder, add the Event
Source provider to the app's logging configuration.

For more information, see:

Trace for performance analysis utility (dotnet-trace) (.NET Core documentation)


Trace for performance analysis utility (dotnet-trace) (dotnet/diagnostics GitHub
repository documentation)
LoggingEventSource
EventLevel
Perfview: Useful for viewing Event Source traces.

Perfview
Use the PerfView utility to collect and view logs. There are other tools for viewing ETW
logs, but PerfView provides the best experience for working with the ETW events
emitted by ASP.NET Core.

To configure PerfView for collecting events logged by this provider, add the string
*Microsoft-Extensions-Logging to the Additional Providers list. Don't miss the * at the
start of the string.

Windows EventLog
The EventLog provider sends log output to the Windows Event Log. Unlike the other
providers, the EventLog provider does not inherit the default non-provider settings. If
EventLog log settings aren't specified, they default to LogLevel.Warning.
To log events lower than LogLevel.Warning, explicitly set the log level. The following
example sets the Event Log default log level to LogLevel.Information:

JSON

"Logging": {
"EventLog": {
"LogLevel": {
"Default": "Information"
}
}
}

AddEventLog overloads can pass in EventLogSettings. If null or not specified, the


following default settings are used:

LogName : "Application"

SourceName : ".NET Runtime"


MachineName : The local machine name is used.

The following code changes the SourceName from the default value of ".NET Runtime" to
MyLogs :

C#

var builder = WebApplication.CreateBuilder();


builder.Logging.AddEventLog(eventLogSettings =>
{
eventLogSettings.SourceName = "MyLogs";
});

Azure App Service


The Microsoft.Extensions.Logging.AzureAppServices provider package writes logs to
text files in an Azure App Service app's file system and to blob storage in an Azure
Storage account.

The provider package isn't included in the shared framework. To use the provider, add
the provider package to the project.

To configure provider settings, use AzureFileLoggerOptions and


AzureBlobLoggerOptions, as shown in the following example:

C#
using Microsoft.Extensions.Logging.AzureAppServices;

var builder = WebApplication.CreateBuilder();


builder.Logging.AddAzureWebAppDiagnostics();
builder.Services.Configure<AzureFileLoggerOptions>(options =>
{
options.FileName = "azure-diagnostics-";
options.FileSizeLimit = 50 * 1024;
options.RetainedFileCountLimit = 5;
});
builder.Services.Configure<AzureBlobLoggerOptions>(options =>
{
options.BlobName = "log.txt";
});

When deployed to Azure App Service, the app uses the settings in the App Service logs
section of the App Service page of the Azure portal. When the following settings are
updated, the changes take effect immediately without requiring a restart or
redeployment of the app.

Application Logging (Filesystem)


Application Logging (Blob)

The default location for log files is in the D:\\home\\LogFiles\\Application folder, and
the default file name is diagnostics-yyyymmdd.txt . The default file size limit is 10 MB,
and the default maximum number of files retained is 2. The default blob name is {app-
name}{timestamp}/yyyy/mm/dd/hh/{guid}-applicationLog.txt .

This provider only logs when the project runs in the Azure environment.

Azure log streaming

Azure log streaming supports viewing log activity in real time from:

The app server


The web server
Failed request tracing

To configure Azure log streaming:

Navigate to the App Service logs page from the app's portal page.
Set Application Logging (Filesystem) to On.
Choose the log Level. This setting only applies to Azure log streaming.

Navigate to the Log Stream page to view logs. The logged messages are logged with
the ILogger interface.
Azure Application Insights
The Microsoft.Extensions.Logging.ApplicationInsights provider package writes logs to
Azure Application Insights. Application Insights is a service that monitors a web app and
provides tools for querying and analyzing the telemetry data. If you use this provider,
you can query and analyze your logs by using the Application Insights tools.

The logging provider is included as a dependency of


Microsoft.ApplicationInsights.AspNetCore , which is the package that provides all
available telemetry for ASP.NET Core. If you use this package, you don't have to install
the provider package.

The Microsoft.ApplicationInsights.Web package is for ASP.NET 4.x, not ASP.NET Core.

For more information, see the following resources:

Application Insights overview


Application Insights for ASP.NET Core applications: Start here if you want to
implement the full range of Application Insights telemetry along with logging.
ApplicationInsightsLoggerProvider for .NET Core ILogger logs: Start here if you
want to implement the logging provider without the rest of Application Insights
telemetry.
Application Insights logging adapters
Install, configure, and initialize the Application Insights SDK interactive tutorial.

Third-party logging providers


Third-party logging frameworks that work with ASP.NET Core:

elmah.io (GitHub repo )


Gelf (GitHub repo )
JSNLog (GitHub repo )
KissLog.net (GitHub repo )
Log4Net (GitHub repo )
NLog (GitHub repo )
PLogger (GitHub repo )
Sentry (GitHub repo )
Serilog (GitHub repo )
Stackdriver (Github repo )

Some third-party frameworks can perform semantic logging, also known as structured
logging .
Using a third-party framework is similar to using one of the built-in providers:

1. Add a NuGet package to your project.


2. Call an ILoggerFactory extension method provided by the logging framework.

For more information, see each provider's documentation. Third-party logging providers
aren't supported by Microsoft.

No asynchronous logger methods


Logging should be so fast that it isn't worth the performance cost of asynchronous
code. If a logging data store is slow, don't write to it directly. Consider writing the log
messages to a fast store initially, then moving them to the slow store later. For example,
when logging to SQL Server, don't do so directly in a Log method, since the Log
methods are synchronous. Instead, synchronously add log messages to an in-memory
queue and have a background worker pull the messages out of the queue to do the
asynchronous work of pushing data to SQL Server. For more information, see Guidance
on how to log to a message queue for slow data stores (dotnet/AspNetCore.Docs
#11801) .

Change log levels in a running app


The Logging API doesn't include a scenario to change log levels while an app is running.
However, some configuration providers are capable of reloading configuration, which
takes immediate effect on logging configuration. For example, the File Configuration
Provider, reloads logging configuration by default. If configuration is changed in code
while an app is running, the app can call IConfigurationRoot.Reload to update the app's
logging configuration.

ILogger and ILoggerFactory


The ILogger<TCategoryName> and ILoggerFactory interfaces and implementations are
included in the .NET Core SDK. They are also available in the following NuGet packages:

The interfaces are in Microsoft.Extensions.Logging.Abstractions .


The default implementations are in Microsoft.Extensions.Logging .

Apply log filter rules in code


The preferred approach for setting log filter rules is by using Configuration.
The following example shows how to register filter rules in code:

C#

using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Debug;

var builder = WebApplication.CreateBuilder();


builder.Logging.AddFilter("System", LogLevel.Debug);
builder.Logging.AddFilter<DebugLoggerProvider>("Microsoft",
LogLevel.Information);
builder.Logging.AddFilter<ConsoleLoggerProvider>("Microsoft",
LogLevel.Trace);

logging.AddFilter("System", LogLevel.Debug) specifies the System category and log

level Debug . The filter is applied to all providers because a specific provider was not
configured.

AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Information) specifies:

The Debug logging provider.


Log level Information and higher.
All categories starting with "Microsoft" .

Automatically log scope with SpanId , TraceId ,


ParentId , Baggage , and Tags .
The logging libraries implicitly create a scope object with SpanId , TraceId ,
ParentId , Baggage , and Tags . This behavior is configured via ActivityTrackingOptions.

C#

var loggerFactory = LoggerFactory.Create(logging =>


{
logging.Configure(options =>
{
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
|
ActivityTrackingOptions.TraceId
|
ActivityTrackingOptions.ParentId
|
ActivityTrackingOptions.Baggage
|
ActivityTrackingOptions.Tags;
}).AddSimpleConsole(options =>
{
options.IncludeScopes = true;
});
});

If the traceparent http request header is set, the ParentId in the log scope shows the
W3C parent-id from in-bound traceparent header and the SpanId in the log scope
shows the updated parent-id for the next out-bound step/span. For more information,
see Mutating the traceparent Field .

Create a custom logger


To create a custom logger, see Implement a custom logging provider in .NET.

Additional resources
Microsoft.Extensions.Logging source on GitHub
View or download sample code (how to download).
High performance logging
Logging bugs should be created in the dotnet/runtime GitHub repository.
ASP.NET Core Blazor logging
HTTP Logging in ASP.NET Core
Article • 03/31/2023

HTTP Logging is a middleware that logs information about incoming HTTP requests and
HTTP responses. HTTP logging provides logs of:

HTTP request information


Common properties
Headers
Body
HTTP response information

HTTP Logging is valuable in several scenarios to:

Record information about incoming requests and responses.


Filter which parts of the request and response are logged.
Filtering which headers to log.

HTTP Logging can reduce the performance of an app, especially when logging the
request and response bodies. Consider the performance impact when selecting fields to
log. Test the performance impact of the selected logging properties.

2 Warning

HTTP Logging can potentially log personally identifiable information (PII). Consider
the risk and avoid logging sensitive information.

Enabling HTTP logging


HTTP Logging is enabled with UseHttpLogging, which adds HTTP logging middleware.

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

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

app.Run();

By default, HTTP Logging logs common properties such as path, status-code, and
headers for requests and responses. Add the following line to the
appsettings.Development.json file at the "LogLevel": { level so the HTTP logs are

displayed:

JSON

"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"

The output is logged as a single message at LogLevel.Information .

HTTP Logging options


To configure the HTTP logging middleware, call AddHttpLogging in Program.cs .

C#

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestHeaders.Add("sec-ch-ua");
logging.ResponseHeaders.Add("MyResponseHeader");
logging.MediaTypeOptions.AddText("application/javascript");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;

});
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

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


{
context.Response.Headers["MyResponseHeader"] =
new string[] { "My Response Header Value" };

await next();
});

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

app.Run();

7 Note

In the preceding sample and following samples, UseHttpLogging is called after


UseStaticFiles , so HTTP logging is not enabled for static file. To enable static file
HTTP logging, call UseHttpLogging before UseStaticFiles .

LoggingFields

HttpLoggingOptions.LoggingFields is an enum flag that configures specific parts of the


request and response to log. HttpLoggingOptions.LoggingFields defaults to
RequestPropertiesAndHeaders | ResponsePropertiesAndHeaders.

RequestHeaders

Headers are a set of HTTP Request Headers that are allowed to be logged. Header
values are only logged for header names that are in this collection. The following code
logs the request header "sec-ch-ua" . If logging.RequestHeaders.Add("sec-ch-ua"); is
removed, the value of the request header "sec-ch-ua" is redacted. The following
highlighted code calls HttpLoggingOptions.RequestHeaders and
HttpLoggingOptions.ResponseHeaders :
C#

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestHeaders.Add("sec-ch-ua");
logging.ResponseHeaders.Add("MyResponseHeader");
logging.MediaTypeOptions.AddText("application/javascript");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

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


{
context.Response.Headers["MyResponseHeader"] =
new string[] { "My Response Header Value" };

await next();
});

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

app.Run();

MediaTypeOptions

MediaTypeOptions provides configuration for selecting which encoding to use for a


specific media type.

C#

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestHeaders.Add("sec-ch-ua");
logging.ResponseHeaders.Add("MyResponseHeader");
logging.MediaTypeOptions.AddText("application/javascript");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

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


{
context.Response.Headers["MyResponseHeader"] =
new string[] { "My Response Header Value" };

await next();
});

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

app.Run();

This approach can also be used to enable logging for data that is not logged by default
(e.g. form data, which might have a media type such as application/x-www-form-
urlencoded or multipart/form-data ).

MediaTypeOptions methods

AddText
AddBinary
Clear

RequestBodyLogLimit and ResponseBodyLogLimit

RequestBodyLogLimit
ResponseBodyLogLimit
C#

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestHeaders.Add("sec-ch-ua");
logging.ResponseHeaders.Add("MyResponseHeader");
logging.MediaTypeOptions.AddText("application/javascript");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

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


{
context.Response.Headers["MyResponseHeader"] =
new string[] { "My Response Header Value" };

await next();
});

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

app.Run();
W3CLogger in ASP.NET Core
Article • 08/16/2022

W3CLogger is a middleware that writes log files in the W3C standard format . The logs
contain information about HTTP requests and HTTP responses. W3CLogger provides
logs of:

HTTP request information


Common properties
Headers
HTTP response information
Metadata about the request/response pair (date/time started, time taken)

W3CLogger is valuable in several scenarios to:

Record information about incoming requests and responses.


Filter which parts of the request and response are logged.
Filter which headers to log.

W3CLogger can reduce the performance of an app. Consider the performance impact
when selecting fields to log - the performance reduction will increase as you log more
properties. Test the performance impact of the selected logging properties.

2 Warning

W3CLogger can potentially log personally identifiable information (PII). Consider


the risk and avoid logging sensitive information. By default, fields that could
contain PII aren't logged.

Enable W3CLogger
Enable W3CLogger with UseW3CLogging, which adds the W3CLogger middleware:

C#

var app = builder.Build();

app.UseW3CLogging();

app.UseRouting();
By default, W3CLogger logs common properties such as path, status-code, date, time,
and protocol. All information about a single request/response pair is written to the same
line.

#Version: 1.0
#Start-Date: 2021-09-29 22:18:28
#Fields: date time c-ip s-computername s-ip s-port cs-method cs-uri-stem cs-
uri-query sc-status time-taken cs-version cs-host cs(User-Agent) cs(Referer)
2021-09-29 22:18:28 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 59.9171
HTTP/1.1 localhost:5000 Mozilla/5.0+
(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+
(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -
2021-09-29 22:18:28 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 0.1802 HTTP/1.1
localhost:5000 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+
(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -
2021-09-29 22:18:30 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 0.0966 HTTP/1.1
localhost:5000 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+
(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -

W3CLogger options
To configure the W3CLogger middleware, call AddW3CLogging in Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddW3CLogging(logging =>
{
// Log all W3C fields
logging.LoggingFields = W3CLoggingFields.All;

logging.AdditionalRequestHeaders.Add("x-forwarded-for");
logging.AdditionalRequestHeaders.Add("x-client-ssl-protocol");
logging.FileSizeLimit = 5 * 1024 * 1024;
logging.RetainedFileCountLimit = 2;
logging.FileName = "MyLogFile";
logging.LogDirectory = @"C:\logs";
logging.FlushInterval = TimeSpan.FromSeconds(2);
});

LoggingFields

W3CLoggerOptions.LoggingFields is a bit flag enumeration that configures specific parts


of the request and response to log, and other information about the connection.
LoggingFields defaults to include all possible fields except UserName and Cookie . For a

complete list of available fields, see W3CLoggingFields.


Use HttpContext in ASP.NET Core
Article • 12/21/2022

HttpContext encapsulates all information about an individual HTTP request and


response. An HttpContext instance is initialized when an HTTP request is received. The
HttpContext instance is accessible by middleware and app frameworks such as Web API
controllers, Razor Pages, SignalR, gRPC, and more.

For more information about accessing the HttpContext , see Access HttpContext in
ASP.NET Core.

HttpRequest
HttpContext.Request provides access to HttpRequest. HttpRequest has information
about the incoming HTTP request, and it's initialized when an HTTP request is received
by the server. HttpRequest isn't read-only, and middleware can change request values in
the middleware pipeline.

Commonly used members on HttpRequest include:

Property Description Example

HttpRequest.Path The request path. /en/article/getstarted

HttpRequest.Method The request method. GET

HttpRequest.Headers A collection of request headers. user-agent=Edge


x-custom-
header=MyValue

HttpRequest.RouteValues A collection of route values. The language=en


collection is set when the request is article=getstarted
matched to a route.

HttpRequest.Query A collection of query values parsed filter=hello


from QueryString. page=1

HttpRequest.ReadFormAsync() A method that reads the request body email=user@contoso.com


as a form and returns a form values password=TNkt4taM
collection. For information about why
ReadFormAsync should be used to
access form data, see Prefer
ReadFormAsync over Request.Form.
Property Description Example

HttpRequest.Body A Stream for reading the request UTF-8 JSON payload


body.

Get request headers


HttpRequest.Headers provides access to the request headers sent with the HTTP
request. There are two ways to access headers using this collection:

Provide the header name to the indexer on the header collection. The header
name isn't case-sensitive. The indexer can access any header value.
The header collection also has properties for getting and setting commonly used
HTTP headers. The properties provide a fast, IntelliSense driven way to access
headers.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", (HttpRequest request) =>


{
var userAgent = request.Headers.UserAgent;
var customHeader = request.Headers["x-custom-header"];

return Results.Ok(new { userAgent = userAgent, customHeader =


customHeader });
});

app.Run();

Read request body


An HTTP request can include a request body. The request body is data associated with
the request, such as the content of an HTML form, UTF-8 JSON payload, or a file.

HttpRequest.Body allows the request body to be read with Stream:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpContext


context) =>
{
var filePath = Path.Combine(config["StoredFilesPath"],
Path.GetRandomFileName());

await using var writeStream = File.Create(filePath);


await context.Request.Body.CopyToAsync(writeStream);
});

app.Run();

HttpRequest.Body can be read directly or used with other APIs that accept stream.

7 Note

Minimal APIs supports binding HttpRequest.Body directly to a Stream parameter.

Enable request body buffering


The request body can only be read once, from beginning to end. Forward-only reading
of the request body avoids the overhead of buffering the entire request body and
reduces memory usage. However, in some scenarios, there's a need to read the request
body multiple times. For example, middleware might need to read the request body and
then rewind it so it's available for the endpoint.

The EnableBuffering extension method enables buffering of the HTTP request body and
is the recommended way to enable multiple reads. Because a request can be any size,
EnableBuffering supports options for buffering large request bodies to disk, or rejecting

them entirely.

The middleware in the following example:

Enables multiple reads with EnableBuffering . It must be called before reading the
request body.
Reads the request body.
Rewinds the request body to the start so other middleware or the endpoint can
read it.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

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


{
context.Request.EnableBuffering();
await ReadRequestBody(context.Request.Body);
context.Request.Body.Position = 0;

await next.Invoke();
});

app.Run();

BodyReader
An alternative way to read the request body is to use the HttpRequest.BodyReader
property. The BodyReader property exposes the request body as a PipeReader. This API
is from I/O pipelines, an advanced, high-performance way to read the request body.

The reader directly accesses the request body and manages memory on the caller's
behalf. Unlike HttpRequest.Body , the reader doesn't copy request data into a buffer.
However, a reader is more complicated to use than a stream and should be used with
caution.

For information on how to read content from BodyReader , see I/O pipelines PipeReader.

HttpResponse
HttpContext.Response provides access to HttpResponse. HttpResponse is used to set
information on the HTTP response sent back to the client.

Commonly used members on HttpResponse include:

Property Description Example

HttpResponse.StatusCode The response code. Must be set before writing 200


to the response body.

HttpResponse.ContentType The response content-type header. Must be set application/json


before writing to the response body.

HttpResponse.Headers A collection of response headers. Must be set server=Kestrel


before writing to the response body. x-custom-
header=MyValue

HttpResponse.Body A Stream for writing the response body. Generated web


page

Set response headers


HttpResponse.Headers provides access to the response headers sent with the HTTP
response. There are two ways to access headers using this collection:

Provide the header name to the indexer on the header collection. The header
name isn't case-sensitive. The indexer can access any header value.
The header collection also has properties for getting and setting commonly used
HTTP headers. The properties provide a fast, IntelliSense driven way to access
headers.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", (HttpResponse response) =>


{
response.Headers.CacheControl = "no-cache";
response.Headers["x-custom-header"] = "Custom value";

return Results.File(File.OpenRead("helloworld.txt"));
});

app.Run();

An app can't modify headers after the response has started. Once the response starts,
the headers are sent to the client. A response is started by flushing the response body or
calling HttpResponse.StartAsync(CancellationToken). The HttpResponse.HasStarted
property indicates whether the response has started. An error is thrown when
attempting to modify headers after the response has started:

System.InvalidOperationException: Headers are read-only, response has already


started.

Write response body


An HTTP response can include a response body. The response body is data associated
with the response, such as generated web page content, UTF-8 JSON payload, or a file.

HttpResponse.Body allows the response body to be written with Stream:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapPost("/downloadfile", async (IConfiguration config, HttpContext


context) =>
{
var filePath = Path.Combine(config["StoredFilesPath"],
"helloworld.txt");

await using var fileStream = File.OpenRead(filePath);


await fileStream.CopyToAsync(context.Response.Body);
});

app.Run();

HttpResponse.Body can be written directly or used with other APIs that write to a stream.

BodyWriter

An alternative way to write the response body is to use the HttpResponse.BodyWriter


property. The BodyWriter property exposes the response body as a PipeWriter. This API
is from I/O pipelines, and it's an advanced, high-performance way to write the response.

The writer provides direct access to the response body and manages memory on the
caller's behalf. Unlike HttpResponse.Body , the write doesn't copy request data into a
buffer. However, a writer is more complicated to use than a stream and writer code
should be thoroughly tested.

For information on how to write content to BodyWriter , see I/O pipelines PipeWriter.

Set response trailers


HTTP/2 and HTTP/3 support response trailers. Trailers are headers sent with the
response after the response body is complete. Because trailers are sent after the
response body, trailers can be added to the response at any time.

The following code sets trailers using AppendTrailer:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", (HttpResponse response) =>


{
// Write body
response.WriteAsync("Hello world");

if (response.SupportsTrailers())
{
response.AppendTrailer("trailername", "TrailerValue");
}
});

app.Run();

RequestAborted
The HttpContext.RequestAborted cancellation token can be used to notify that the HTTP
request has been aborted by the client or server. The cancellation token should be
passed to long-running tasks so they can be canceled if the request is aborted. For
example, aborting a database query or HTTP request to get data to return in the
response.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

var httpClient = new HttpClient();


app.MapPost("/books/{bookId}", async (int bookId, HttpContext context) =>
{
var stream = await httpClient.GetStreamAsync(
$"http://consoto/books/{bookId}.json", context.RequestAborted);

// Proxy the response as JSON


return Results.Stream(stream, "application/json");
});

app.Run();

The RequestAborted cancellation token doesn't need to be used for request body read
operations because reads always throw immediately when the request is aborted. The
RequestAborted token is also usually unnecessary when writing response bodies,
because writes immediately no-op when the request is aborted.

In some cases, passing the RequestAborted token to write operations can be a


convenient way to force a write loop to exit early with an OperationCanceledException.
However, it's typically better to pass the RequestAborted token into any asynchronous
operations responsible for retrieving the response body content instead.

7 Note

Minimal APIs supports binding HttpContext.RequestAborted directly to a


CancellationToken parameter.
Abort()
The HttpContext.Abort() method can be used to abort an HTTP request from the server.
Aborting the HTTP request immediately triggers the HttpContext.RequestAborted
cancellation token and sends a notification to the client that the server has aborted the
request.

The middleware in the following example:

Adds a custom check for malicious requests.


Aborts the HTTP request if the request is malicious.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

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


{
if (RequestAppearsMalicious(context.Request))
{
// Malicious requests don't even deserve an error response (e.g.
400).
context.Abort();
return;
}

await next.Invoke();
});

app.Run();

User
The HttpContext.User property is used to get or set the user, represented by
ClaimsPrincipal, for the request. The ClaimsPrincipal is typically set by ASP.NET Core
authentication.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/user/current", [Authorize] async (HttpContext context) =>


{
var user = await GetUserAsync(context.User.Identity.Name);
return Results.Ok(user);
});

app.Run();

7 Note

Minimal APIs supports binding HttpContext.User directly to a ClaimsPrincipal


parameter.

Features
The HttpContext.Features property provides access to the collection of feature interfaces
for the current request. Since the feature collection is mutable even within the context of
a request, middleware can be used to modify the collection and add support for
additional features. Some advanced features are only available by accessing the
associated interface through the feature collection.

The following example:

Gets IHttpMinRequestBodyDataRateFeature from the features collection.


Sets MinDataRate to null. This removes the minimum data rate that the request
body must be sent by the client for this HTTP request.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/long-running-stream", async (HttpContext context) =>


{
var feature =
httpContext.Features.Get<IHttpMinRequestBodyDataRateFeature>();
if (feature != null)
{
feature.MinDataRate = null;
}

// Read long-running stream from request body.


});

app.Run();

For more information about using request features and HttpContext , see Request
Features in ASP.NET Core.
HttpContext isn't thread safe
This article primarily discusses using HttpContext in request and response flow from
Razor Pages, controllers, middleware, etc. Consider the following when using
HttpContext outside the request and response flow:

The HttpContext is NOT thread safe, accessing it from multiple threads can result
in exceptions, data corruption and generally unpredictable results.
The IHttpContextAccessor interface should be used with caution. As always, the
HttpContext must not be captured outside of the request flow.

IHttpContextAccessor :
Relies on AsyncLocal<T> which can have a negative performance impact on
asynchronous calls.
Creates a dependency on "ambient state" which can make testing more difficult.
IHttpContextAccessor.HttpContext may be null if accessed outside of the request
flow.
To access information from HttpContext outside the request flow, copy the
information inside the request flow. Be careful to copy the actual data and not just
references. For example, rather than copying a reference to an IHeaderDictionary ,
copy the relevant header values or copy the entire dictionary key by key before
leaving the request flow.
Don't capture IHttpContextAccessor.HttpContext in a constructor.

The following sample logs GitHub branches when requested from the /branch endpoint:

C#

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>


{
httpClient.BaseAddress = new Uri("https://api.github.com/");

// The GitHub API requires two headers. The Use-Agent header is added
// dynamically through UserAgentHeaderHandler
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();
builder.Services.AddTransient<UserAgentHeaderHandler>();

var app = builder.Build();

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

app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,


HttpContext context, Logger<Program> logger) =>
{
var httpClient = httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");

if (!httpResponseMessage.IsSuccessStatusCode)
return Results.BadRequest();

await using var contentStream =


await httpResponseMessage.Content.ReadAsStreamAsync();

var response = await JsonSerializer.DeserializeAsync


<IEnumerable<GitHubBranch>>(contentStream);

app.Logger.LogInformation($"/branches request: " +


$"{JsonSerializer.Serialize(response)}");

return Results.Ok(response);
});

app.Run();

The GitHub API requires two headers. The User-Agent header is added dynamically by
the UserAgentHeaderHandler :

C#

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");

// The GitHub API requires two headers. The Use-Agent header is added
// dynamically through UserAgentHeaderHandler
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();

builder.Services.AddTransient<UserAgentHeaderHandler>();

var app = builder.Build();

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

app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,


HttpContext context, Logger<Program> logger) =>
{
var httpClient = httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");

if (!httpResponseMessage.IsSuccessStatusCode)
return Results.BadRequest();

await using var contentStream =


await httpResponseMessage.Content.ReadAsStreamAsync();

var response = await JsonSerializer.DeserializeAsync


<IEnumerable<GitHubBranch>>(contentStream);

app.Logger.LogInformation($"/branches request: " +


$"{JsonSerializer.Serialize(response)}");

return Results.Ok(response);
});

app.Run();

The UserAgentHeaderHandler :

C#

using Microsoft.Net.Http.Headers;

namespace HttpContextInBackgroundThread;

public class UserAgentHeaderHandler : DelegatingHandler


{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger _logger;
public UserAgentHeaderHandler(IHttpContextAccessor httpContextAccessor,
ILogger<UserAgentHeaderHandler> logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}

protected override async Task<HttpResponseMessage>


SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var contextRequest = _httpContextAccessor.HttpContext?.Request;
string? userAgentString = contextRequest?.Headers["user-
agent"].ToString();

if (string.IsNullOrEmpty(userAgentString))
{
userAgentString = "Unknown";
}

request.Headers.Add(HeaderNames.UserAgent, userAgentString);
_logger.LogInformation($"User-Agent: {userAgentString}");

return await base.SendAsync(request, cancellationToken);


}
}

In the preceding code, when the HttpContext is null , the userAgent string is set to
"Unknown" . If possible, HttpContext should be explicitly passed to the service. Explicitly

passing in HttpContext data:

Makes the service API more useable outside the request flow.
Is better for performance.
Makes the code easier to understand and reason about than relying on ambient
state.

When the service must access HttpContext , it should account for the possibility of
HttpContext being null when not called from a request thread.

The application also includes PeriodicBranchesLoggerService , which logs the open


GitHub branches of the specified repository every 30 seconds:

C#

using System.Text.Json;

namespace HttpContextInBackgroundThread;

public class PeriodicBranchesLoggerService : BackgroundService


{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
private readonly PeriodicTimer _timer;

public PeriodicBranchesLoggerService(IHttpClientFactory
httpClientFactory,

ILogger<PeriodicBranchesLoggerService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
}

protected override async Task ExecuteAsync(CancellationToken


stoppingToken)
{
while (await _timer.WaitForNextTickAsync(stoppingToken))
{
try
{
// Cancel sending the request to sync branches if it takes
too long
// rather than miss sending the next request scheduled 30
seconds from now.
// Having a single loop prevents this service from sending
an unbounded
// number of requests simultaneously.
using var syncTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
syncTokenSource.CancelAfter(TimeSpan.FromSeconds(30));

var httpClient = _httpClientFactory.CreateClient("GitHub");


var httpResponseMessage = await
httpClient.GetAsync("repos/dotnet/AspNetCore.Docs/branches",

stoppingToken);

if (httpResponseMessage.IsSuccessStatusCode)
{
await using var contentStream =
await
httpResponseMessage.Content.ReadAsStreamAsync(stoppingToken);

// Sync the response with preferred datastore.


var response = await JsonSerializer.DeserializeAsync<
IEnumerable<GitHubBranch>>(contentStream,
cancellationToken: stoppingToken);

_logger.LogInformation(
$"Branch sync successful! Response:
{JsonSerializer.Serialize(response)}");
}
else
{
_logger.LogError(1, $"Branch sync failed! HTTP status
code: {httpResponseMessage.StatusCode}");
}
}
catch (Exception ex)
{
_logger.LogError(1, ex, "Branch sync failed!");
}
}
}

public override Task StopAsync(CancellationToken stoppingToken)


{
// This will cause any active call to WaitForNextTickAsync() to
return false immediately.
_timer.Dispose();
// This will cancel the stoppingToken and await
ExecuteAsync(stoppingToken).
return base.StopAsync(stoppingToken);
}
}

PeriodicBranchesLoggerService is a hosted service, which runs outside the request and


response flow. Logging from the PeriodicBranchesLoggerService has a null HttpContext .
The PeriodicBranchesLoggerService was written to not depend on the HttpContext .

C#

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>


{
Routing in ASP.NET Core
Article • 05/21/2023

By Ryan Nowak , Kirk Larkin , and Rick Anderson

Routing is responsible for matching incoming HTTP requests and dispatching those
requests to the app's executable endpoints. Endpoints are the app's units of executable
request-handling code. Endpoints are defined in the app and configured when the app
starts. The endpoint matching process can extract values from the request's URL and
provide those values for request processing. Using endpoint information from the app,
routing is also able to generate URLs that map to endpoints.

Apps can configure routing using:

Controllers
Razor Pages
SignalR
gRPC Services
Endpoint-enabled middleware such as Health Checks.
Delegates and lambdas registered with routing.

This article covers low-level details of ASP.NET Core routing. For information on
configuring routing:

For controllers, see Routing to controller actions in ASP.NET Core.


For Razor Pages conventions, see Razor Pages route and app conventions in
ASP.NET Core.

Routing basics
The following code shows a basic example of routing:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

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

app.Run();

The preceding example includes a single endpoint using the MapGet method:
When an HTTP GET request is sent to the root URL / :
The request delegate executes.
Hello World! is written to the HTTP response.

If the request method is not GET or the root URL is not / , no route matches and
an HTTP 404 is returned.

Routing uses a pair of middleware, registered by UseRouting and UseEndpoints:

UseRouting adds route matching to the middleware pipeline. This middleware


looks at the set of endpoints defined in the app, and selects the best match based
on the request.
UseEndpoints adds endpoint execution to the middleware pipeline. It runs the

delegate associated with the selected endpoint.

Apps typically don't need to call UseRouting or UseEndpoints . WebApplicationBuilder


configures a middleware pipeline that wraps middleware added in Program.cs with
UseRouting and UseEndpoints . However, apps can change the order in which
UseRouting and UseEndpoints run by calling these methods explicitly. For example, the

following code makes an explicit call to UseRouting :

C#

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


{
// ...
await next(context);
});

app.UseRouting();

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

In the preceding code:

The call to app.Use registers a custom middleware that runs at the start of the
pipeline.
The call to UseRouting configures the route matching middleware to run after the
custom middleware.
The endpoint registered with MapGet runs at the end of the pipeline.

If the preceding example didn't include a call to UseRouting , the custom middleware
would run after the route matching middleware.
Endpoints
The MapGet method is used to define an endpoint. An endpoint is something that can
be:

Selected, by matching the URL and HTTP method.


Executed, by running the delegate.

Endpoints that can be matched and executed by the app are configured in
UseEndpoints . For example, MapGet, MapPost, and similar methods connect request
delegates to the routing system. Additional methods can be used to connect ASP.NET
Core framework features to the routing system:

MapRazorPages for Razor Pages


MapControllers for controllers
MapHub<THub> for SignalR
MapGrpcService<TService> for gRPC

The following example shows routing with a more sophisticated route template:

C#

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

The string /hello/{name:alpha} is a route template. A route template is used to


configure how the endpoint is matched. In this case, the template matches:

A URL like /hello/Docs


Any URL path that begins with /hello/ followed by a sequence of alphabetic
characters. :alpha applies a route constraint that matches only alphabetic
characters. Route constraints are explained later in this article.

The second segment of the URL path, {name:alpha} :

Is bound to the name parameter.


Is captured and stored in HttpRequest.RouteValues.

The following example shows routing with health checks and authorization:

C#

app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

The preceding example demonstrates how:

The authorization middleware can be used with routing.


Endpoints can be used to configure authorization behavior.

The MapHealthChecks call adds a health check endpoint. Chaining RequireAuthorization


on to this call attaches an authorization policy to the endpoint.

Calling UseAuthentication and UseAuthorization adds the authentication and


authorization middleware. These middleware are placed between UseRouting and
UseEndpoints so that they can:

See which endpoint was selected by UseRouting .


Apply an authorization policy before UseEndpoints dispatches to the endpoint.

Endpoint metadata
In the preceding example, there are two endpoints, but only the health check endpoint
has an authorization policy attached. If the request matches the health check endpoint,
/healthz , an authorization check is performed. This demonstrates that endpoints can

have extra data attached to them. This extra data is called endpoint metadata:

The metadata can be processed by routing-aware middleware.


The metadata can be of any .NET type.

Routing concepts
The routing system builds on top of the middleware pipeline by adding the powerful
endpoint concept. Endpoints represent units of the app's functionality that are distinct
from each other in terms of routing, authorization, and any number of ASP.NET Core's
systems.

ASP.NET Core endpoint definition


An ASP.NET Core endpoint is:

Executable: Has a RequestDelegate.


Extensible: Has a Metadata collection.
Selectable: Optionally, has routing information.
Enumerable: The collection of endpoints can be listed by retrieving the
EndpointDataSource from DI.

The following code shows how to retrieve and inspect the endpoint matching the
current request:

C#

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


{
var currentEndpoint = context.GetEndpoint();

if (currentEndpoint is null)
{
await next(context);
return;
}

Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

if (currentEndpoint is RouteEndpoint routeEndpoint)


{
Console.WriteLine($" - Route Pattern:
{routeEndpoint.RoutePattern}");
}

foreach (var endpointMetadata in currentEndpoint.Metadata)


{
Console.WriteLine($" - Metadata: {endpointMetadata}");
}

await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

The endpoint, if selected, can be retrieved from the HttpContext . Its properties can be
inspected. Endpoint objects are immutable and cannot be modified after creation. The
most common type of endpoint is a RouteEndpoint. RouteEndpoint includes information
that allows it to be selected by the routing system.

In the preceding code, app.Use configures an inline middleware.

The following code shows that, depending on where app.Use is called in the pipeline,
there may not be an endpoint:

C#
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ??
"(null)"}");
await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing


found a match.
app.Use(async (context, next) =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ??
"(null)"}");
await next(context);
});

// Location 3: runs when this endpoint matches


app.MapGet("/", (HttpContext context) =>
{
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ??
"(null)"}");
return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no


match.
app.Use(async (context, next) =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ??
"(null)"}");
await next(context);
});

The preceding sample adds Console.WriteLine statements that display whether or not
an endpoint has been selected. For clarity, the sample assigns a display name to the
provided / endpoint.

The preceding sample also includes calls to UseRouting and UseEndpoints to control
exactly when these middleware run within the pipeline.

Running this code with a URL of / displays:

txt
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Running this code with any other URL displays:

txt

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

This output demonstrates that:

The endpoint is always null before UseRouting is called.


If a match is found, the endpoint is non-null between UseRouting and
UseEndpoints.
The UseEndpoints middleware is terminal when a match is found. Terminal
middleware is defined later in this article.
The middleware after UseEndpoints execute only when no match is found.

The UseRouting middleware uses the SetEndpoint method to attach the endpoint to the
current context. It's possible to replace the UseRouting middleware with custom logic
and still get the benefits of using endpoints. Endpoints are a low-level primitive like
middleware, and aren't coupled to the routing implementation. Most apps don't need to
replace UseRouting with custom logic.

The UseEndpoints middleware is designed to be used in tandem with the UseRouting


middleware. The core logic to execute an endpoint isn't complicated. Use GetEndpoint
to retrieve the endpoint, and then invoke its RequestDelegate property.

The following code demonstrates how middleware can influence or react to routing:

C#

app.UseHttpMethodOverride();
app.UseRouting();

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


{
if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>
() is not null)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT:
{DateTime.UtcNow}");
}

await next(context);
});

app.MapGet("/", () => "Audit isn't required.");


app.MapGet("/sensitive", () => "Audit required for sensitive data.")
.WithMetadata(new RequiresAuditAttribute());

C#

public class RequiresAuditAttribute : Attribute { }

The preceding example demonstrates two important concepts:

Middleware can run before UseRouting to modify the data that routing operates
upon.
Usually middleware that appears before routing modifies some property of the
request, such as UseRewriter, UseHttpMethodOverride, or UsePathBase.
Middleware can run between UseRouting and UseEndpoints to process the results
of routing before the endpoint is executed.
Middleware that runs between UseRouting and UseEndpoints :
Usually inspects metadata to understand the endpoints.
Often makes security decisions, as done by UseAuthorization and UseCors .
The combination of middleware and metadata allows configuring policies per-
endpoint.

The preceding code shows an example of a custom middleware that supports per-
endpoint policies. The middleware writes an audit log of access to sensitive data to the
console. The middleware can be configured to audit an endpoint with the
RequiresAuditAttribute metadata. This sample demonstrates an opt-in pattern where
only endpoints that are marked as sensitive are audited. It's possible to define this logic
in reverse, auditing everything that isn't marked as safe, for example. The endpoint
metadata system is flexible. This logic could be designed in whatever way suits the use
case.

The preceding sample code is intended to demonstrate the basic concepts of endpoints.
The sample is not intended for production use. A more complete version of an audit
log middleware would:

Log to a file or database.


Include details such as the user, IP address, name of the sensitive endpoint, and
more.
The audit policy metadata RequiresAuditAttribute is defined as an Attribute for easier
use with class-based frameworks such as controllers and SignalR. When using route to
code:

Metadata is attached with a builder API.


Class-based frameworks include all attributes on the corresponding method and
class when creating endpoints.

The best practices for metadata types are to define them either as interfaces or
attributes. Interfaces and attributes allow code reuse. The metadata system is flexible
and doesn't impose any limitations.

Compare terminal middleware with routing


The following example demonstrates both terminal middleware and routing:

C#

// Approach 1: Terminal Middleware.


app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Terminal Middleware.");
return;
}

await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

The style of middleware shown with Approach 1: is terminal middleware. It's called
terminal middleware because it does a matching operation:

The matching operation in the preceding sample is Path == "/" for the
middleware and Path == "/Routing" for routing.
When a match is successful, it executes some functionality and returns, rather than
invoking the next middleware.

It's called terminal middleware because it terminates the search, executes some
functionality, and then returns.
The following list compares terminal middleware with routing:

Both approaches allow terminating the processing pipeline:


Middleware terminates the pipeline by returning rather than invoking next .
Endpoints are always terminal.
Terminal middleware allows positioning the middleware at an arbitrary place in the
pipeline:
Endpoints execute at the position of UseEndpoints.
Terminal middleware allows arbitrary code to determine when the middleware
matches:
Custom route matching code can be verbose and difficult to write correctly.
Routing provides straightforward solutions for typical apps. Most apps don't
require custom route matching code.
Endpoints interface with middleware such as UseAuthorization and UseCors .
Using a terminal middleware with UseAuthorization or UseCors requires manual
interfacing with the authorization system.

An endpoint defines both:

A delegate to process requests.


A collection of arbitrary metadata. The metadata is used to implement cross-
cutting concerns based on policies and configuration attached to each endpoint.

Terminal middleware can be an effective tool, but can require:

A significant amount of coding and testing.


Manual integration with other systems to achieve the desired level of flexibility.

Consider integrating with routing before writing a terminal middleware.

Existing terminal middleware that integrates with Map or MapWhen can usually be
turned into a routing aware endpoint. MapHealthChecks demonstrates the pattern for
router-ware:

Write an extension method on IEndpointRouteBuilder.


Create a nested middleware pipeline using CreateApplicationBuilder.
Attach the middleware to the new pipeline. In this case, UseHealthChecks.
Build the middleware pipeline into a RequestDelegate.
Call Map and provide the new middleware pipeline.
Return the builder object provided by Map from the extension method.

The following code shows use of MapHealthChecks:

C#
app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

The preceding sample shows why returning the builder object is important. Returning
the builder object allows the app developer to configure policies such as authorization
for the endpoint. In this example, the health checks middleware has no direct
integration with the authorization system.

The metadata system was created in response to the problems encountered by


extensibility authors using terminal middleware. It's problematic for each middleware to
implement its own integration with the authorization system.

URL matching
Is the process by which routing matches an incoming request to an endpoint.
Is based on data in the URL path and headers.
Can be extended to consider any data in the request.

When a routing middleware executes, it sets an Endpoint and route values to a request
feature on the HttpContext from the current request:

Calling HttpContext.GetEndpoint gets the endpoint.


HttpRequest.RouteValues gets the collection of route values.

Middleware runs after the routing middleware can inspect the endpoint and take action.
For example, an authorization middleware can interrogate the endpoint's metadata
collection for an authorization policy. After all of the middleware in the request
processing pipeline is executed, the selected endpoint's delegate is invoked.

The routing system in endpoint routing is responsible for all dispatching decisions.
Because the middleware applies policies based on the selected endpoint, it's important
that:

Any decision that can affect dispatching or the application of security policies is
made inside the routing system.

2 Warning

For backward-compatibility, when a Controller or Razor Pages endpoint delegate is


executed, the properties of RouteContext.RouteData are set to appropriate values
based on the request processing performed thus far.
The RouteContext type will be marked obsolete in a future release:

Migrate RouteData.Values to HttpRequest.RouteValues .


Migrate RouteData.DataTokens to retrieve IDataTokensMetadata from the
endpoint metadata.

URL matching operates in a configurable set of phases. In each phase, the output is a set
of matches. The set of matches can be narrowed down further by the next phase. The
routing implementation does not guarantee a processing order for matching endpoints.
All possible matches are processed at once. The URL matching phases occur in the
following order. ASP.NET Core:

1. Processes the URL path against the set of endpoints and their route templates,
collecting all of the matches.
2. Takes the preceding list and removes matches that fail with route constraints
applied.
3. Takes the preceding list and removes matches that fail the set of MatcherPolicy
instances.
4. Uses the EndpointSelector to make a final decision from the preceding list.

The list of endpoints is prioritized according to:

The RouteEndpoint.Order
The route template precedence

All matching endpoints are processed in each phase until the EndpointSelector is
reached. The EndpointSelector is the final phase. It chooses the highest priority
endpoint from the matches as the best match. If there are other matches with the same
priority as the best match, an ambiguous match exception is thrown.

The route precedence is computed based on a more specific route template being
given a higher priority. For example, consider the templates /hello and /{message} :

Both match the URL path /hello .


/hello is more specific and therefore higher priority.

In general, route precedence does a good job of choosing the best match for the kinds
of URL schemes used in practice. Use Order only when necessary to avoid an ambiguity.

Due to the kinds of extensibility provided by routing, it isn't possible for the routing
system to compute ahead of time the ambiguous routes. Consider an example such as
the route templates /{message:alpha} and /{message:int} :
The alpha constraint matches only alphabetic characters.
The int constraint matches only numbers.
These templates have the same route precedence, but there's no single URL they
both match.
If the routing system reported an ambiguity error at startup, it would block this
valid use case.

2 Warning

The order of operations inside UseEndpoints doesn't influence the behavior of


routing, with one exception. MapControllerRoute and MapAreaRoute
automatically assign an order value to their endpoints based on the order they are
invoked. This simulates long-time behavior of controllers without the routing
system providing the same guarantees as older routing implementations.

Endpoint routing in ASP.NET Core:

Doesn't have the concept of routes.


Doesn't provide ordering guarantees. All endpoints are processed at once.

Route template precedence and endpoint selection order


Route template precedence is a system that assigns each route template a value
based on how specific it is. Route template precedence:

Avoids the need to adjust the order of endpoints in common cases.


Attempts to match the common-sense expectations of routing behavior.

For example, consider templates /Products/List and /Products/{id} . It would be


reasonable to assume that /Products/List is a better match than /Products/{id} for
the URL path /Products/List . This works because the literal segment /List is
considered to have better precedence than the parameter segment /{id} .

The details of how precedence works are coupled to how route templates are defined:

Templates with more segments are considered more specific.


A segment with literal text is considered more specific than a parameter segment.
A parameter segment with a constraint is considered more specific than one
without.
A complex segment is considered as specific as a parameter segment with a
constraint.
Catch-all parameters are the least specific. See catch-all in the Route templates
section for important information on catch-all routes.

URL generation concepts


URL generation:

Is the process by which routing can create a URL path based on a set of route
values.
Allows for a logical separation between endpoints and the URLs that access them.

Endpoint routing includes the LinkGenerator API. LinkGenerator is a singleton service


available from DI. The LinkGenerator API can be used outside of the context of an
executing request. Mvc.IUrlHelper and scenarios that rely on IUrlHelper, such as Tag
Helpers, HTML Helpers, and Action Results, use the LinkGenerator API internally to
provide link generating capabilities.

The link generator is backed by the concept of an address and address schemes. An
address scheme is a way of determining the endpoints that should be considered for
link generation. For example, the route name and route values scenarios many users are
familiar with from controllers and Razor Pages are implemented as an address scheme.

The link generator can link to controllers and Razor Pages via the following extension
methods:

GetPathByAction
GetUriByAction
GetPathByPage
GetUriByPage

Overloads of these methods accept arguments that include the HttpContext . These
methods are functionally equivalent to Url.Action and Url.Page, but offer additional
flexibility and options.

The GetPath* methods are most similar to Url.Action and Url.Page , in that they
generate a URI containing an absolute path. The GetUri* methods always generate an
absolute URI containing a scheme and host. The methods that accept an HttpContext
generate a URI in the context of the executing request. The ambient route values, URL
base path, scheme, and host from the executing request are used unless overridden.

LinkGenerator is called with an address. Generating a URI occurs in two steps:

1. An address is bound to a list of endpoints that match the address.


2. Each endpoint's RoutePattern is evaluated until a route pattern that matches the
supplied values is found. The resulting output is combined with the other URI parts
supplied to the link generator and returned.

The methods provided by LinkGenerator support standard link generation capabilities


for any type of address. The most convenient way to use the link generator is through
extension methods that perform operations for a specific address type:

Extension Method Description

GetPathByAddress Generates a URI with an absolute path based on the provided values.

GetUriByAddress Generates an absolute URI based on the provided values.

2 Warning

Pay attention to the following implications of calling LinkGenerator methods:

Use GetUri* extension methods with caution in an app configuration that


doesn't validate the Host header of incoming requests. If the Host header of
incoming requests isn't validated, untrusted request input can be sent back to
the client in URIs in a view or page. We recommend that all production apps
configure their server to validate the Host header against known valid values.

Use LinkGenerator with caution in middleware in combination with Map or


MapWhen . Map* changes the base path of the executing request, which affects

the output of link generation. All of the LinkGenerator APIs allow specifying a
base path. Specify an empty base path to undo the Map* affect on link
generation.

Middleware example
In the following example, a middleware uses the LinkGenerator API to create a link to an
action method that lists store products. Using the link generator by injecting it into a
class and calling GenerateLink is available to any class in an app:

C#

public class ProductsMiddleware


{
private readonly LinkGenerator _linkGenerator;
public ProductsMiddleware(RequestDelegate next, LinkGenerator
linkGenerator) =>
_linkGenerator = linkGenerator;

public async Task InvokeAsync(HttpContext httpContext)


{
httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

var productsPath = _linkGenerator.GetPathByAction("Products",


"Store");

await httpContext.Response.WriteAsync(
$"Go to {productsPath} to see our products.");
}
}

Route templates
Tokens within {} define route parameters that are bound if the route is matched. More
than one route parameter can be defined in a route segment, but route parameters
must be separated by a literal value. For example:

{controller=Home}{action=Index}

isn't a valid route, because there's no literal value between {controller} and {action} .
Route parameters must have a name and may have additional attributes specified.

Literal text other than route parameters (for example, {id} ) and the path separator /
must match the text in the URL. Text matching is case-insensitive and based on the
decoded representation of the URL's path. To match a literal route parameter delimiter
{ or } , escape the delimiter by repeating the character. For example {{ or }} .

Asterisk * or double asterisk ** :

Can be used as a prefix to a route parameter to bind to the rest of the URI.
Are called a catch-all parameters. For example, blog/{**slug} :
Matches any URI that starts with blog/ and has any value following it.
The value following blog/ is assigned to the slug route value.

Catch-all parameters can also match the empty string.

The catch-all parameter escapes the appropriate characters when the route is used to
generate a URL, including path separator / characters. For example, the route
foo/{*path} with route values { path = "my/path" } generates foo/my%2Fpath . Note the

escaped forward slash. To round-trip path separator characters, use the ** route
parameter prefix. The route foo/{**path} with { path = "my/path" } generates
foo/my/path .

URL patterns that attempt to capture a file name with an optional file extension have
additional considerations. For example, consider the template files/{filename}.{ext?} .
When values for both filename and ext exist, both values are populated. If only a value
for filename exists in the URL, the route matches because the trailing . is optional. The
following URLs match this route:

/files/myFile.txt

/files/myFile

Route parameters may have default values designated by specifying the default value
after the parameter name separated by an equals sign ( = ). For example,
{controller=Home} defines Home as the default value for controller . The default value is
used if no value is present in the URL for the parameter. Route parameters are made
optional by appending a question mark ( ? ) to the end of the parameter name. For
example, id? . The difference between optional values and default route parameters is:

A route parameter with a default value always produces a value.


An optional parameter has a value only when a value is provided by the request
URL.

Route parameters may have constraints that must match the route value bound from
the URL. Adding : and constraint name after the route parameter name specifies an
inline constraint on a route parameter. If the constraint requires arguments, they're
enclosed in parentheses (...) after the constraint name. Multiple inline constraints can
be specified by appending another : and constraint name.

The constraint name and arguments are passed to the IInlineConstraintResolver service
to create an instance of IRouteConstraint to use in URL processing. For example, the
route template blog/{article:minlength(10)} specifies a minlength constraint with the
argument 10 . For more information on route constraints and a list of the constraints
provided by the framework, see the Route constraints section.

Route parameters may also have parameter transformers. Parameter transformers


transform a parameter's value when generating links and matching actions and pages to
URLs. Like constraints, parameter transformers can be added inline to a route parameter
by adding a : and transformer name after the route parameter name. For example, the
route template blog/{article:slugify} specifies a slugify transformer. For more
information on parameter transformers, see the Parameter transformers section.
The following table demonstrates example route templates and their behavior:

Route Template Example Matching The request URI…


URI

hello /hello Only matches the single


path /hello .

{Page=Home} / Matches and sets Page to


Home .

{Page=Home} /Contact Matches and sets Page to


Contact .

{controller}/{action}/{id?} /Products/List Maps to the Products


controller and List action.

{controller}/{action}/{id?} /Products/Details/123 Maps to the Products


controller and Details
action with id set to 123.

{controller=Home}/{action=Index}/{id?} / Maps to the Home controller


and Index method. id is
ignored.

{controller=Home}/{action=Index}/{id?} /Products Maps to the Products


controller and Index
method. id is ignored.

Using a template is generally the simplest approach to routing. Constraints and defaults
can also be specified outside the route template.

Complex segments
Complex segments are processed by matching up literal delimiters from right to left in a
non-greedy way. For example, [Route("/a{b}c{d}")] is a complex segment. Complex
segments work in a particular way that must be understood to use them successfully.
The example in this section demonstrates why complex segments only really work well
when the delimiter text doesn't appear inside the parameter values. Using a regex and
then manually extracting the values is needed for more complex cases.

2 Warning

When using System.Text.RegularExpressions to process untrusted input, pass a


timeout. A malicious user can provide input to RegularExpressions causing a
Denial-of-Service attack . ASP.NET Core framework APIs that use
RegularExpressions pass a timeout.

This is a summary of the steps that routing performs with the template /a{b}c{d} and
the URL path /abcd . The | is used to help visualize how the algorithm works:

The first literal, right to left, is c . So /abcd is searched from right and finds
/ab|c|d .

Everything to the right ( d ) is now matched to the route parameter {d} .


The next literal, right to left, is a . So /ab|c|d is searched starting where we left off,
then a is found /|a|b|c|d .
The value to the right ( b ) is now matched to the route parameter {b} .
There is no remaining text and no remaining route template, so this is a match.

Here's an example of a negative case using the same template /a{b}c{d} and the URL
path /aabcd . The | is used to help visualize how the algorithm works. This case isn't a
match, which is explained by the same algorithm:

The first literal, right to left, is c . So /aabcd is searched from right and finds
/aab|c|d .

Everything to the right ( d ) is now matched to the route parameter {d} .


The next literal, right to left, is a . So /aab|c|d is searched starting where we left
off, then a is found /a|a|b|c|d .
The value to the right ( b ) is now matched to the route parameter {b} .
At this point there is remaining text a , but the algorithm has run out of route
template to parse, so this is not a match.

Since the matching algorithm is non-greedy:

It matches the smallest amount of text possible in each step.


Any case where the delimiter value appears inside the parameter values results in
not matching.

Regular expressions provide much more control over their matching behavior.

Greedy matching, also known as lazy matching , matches the largest possible string.
Non-greedy matches the smallest possible string.

Routing with special characters


Routing with special characters can lead to unexpected results. For example, consider a
controller with the following action method:

C#

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null || todoItem.Name == null)


{
return NotFound();
}

return todoItem.Name;
}

When string id contains the following encoded values, unexpected results might
occur:

ASCII Encoded

/ %2F

Route parameters are not always URL decoded. This problem may be addressed in the
future. For more information, see this GitHub issue ;

Route constraints
Route constraints execute when a match has occurred to the incoming URL and the URL
path is tokenized into route values. Route constraints generally inspect the route value
associated via the route template and make a true or false decision about whether the
value is acceptable. Some route constraints use data outside the route value to consider
whether the request can be routed. For example, the HttpMethodRouteConstraint can
accept or reject a request based on its HTTP verb. Constraints are used in routing
requests and link generation.

2 Warning

Don't use constraints for input validation. If constraints are used for input
validation, invalid input results in a 404 Not Found response. Invalid input should
produce a 400 Bad Request with an appropriate error message. Route constraints
are used to disambiguate similar routes, not to validate the inputs for a particular
route.

The following table demonstrates example route constraints and their expected
behavior:

constraint Example Example Notes


Matches

int {id:int} 123456789 , Matches any integer


-123456789

bool {active:bool} true , FALSE Matches true or false . Case-


insensitive

datetime {dob:datetime} 2016-12-31 , Matches a valid DateTime value


2016-12-31 in the invariant culture. See
7:32pm preceding warning.

decimal {price:decimal} 49.99 , Matches a valid decimal value


-1,000.01 in the invariant culture. See
preceding warning.

double {weight:double} 1.234 , Matches a valid double value in


-1,001.01e8 the invariant culture. See
preceding warning.

float {weight:float} 1.234 , Matches a valid float value in


-1,001.01e8 the invariant culture. See
preceding warning.

guid {id:guid} CD2C1638- Matches a valid Guid value


1638-72D5-
1638-
DEADBEEF1638

long {ticks:long} 123456789 , Matches a valid long value


-123456789

minlength(value) {username:minlength(4)} Rick String must be at least 4


characters

maxlength(value) {filename:maxlength(8)} MyFile String must be no more than 8


characters

length(length) {filename:length(12)} somefile.txt String must be exactly 12


characters long
constraint Example Example Notes
Matches

length(min,max) {filename:length(8,16)} somefile.txt String must be at least 8 and no


more than 16 characters long

min(value) {age:min(18)} 19 Integer value must be at least


18

max(value) {age:max(120)} 91 Integer value must be no more


than 120

range(min,max) {age:range(18,120)} 91 Integer value must be at least


18 but no more than 120

alpha {name:alpha} Rick String must consist of one or


more alphabetical characters,
a - z and case-insensitive.

regex(expression) {ssn:regex(^\\d{{3}}- 123-45-6789 String must match the regular


\\d{{2}}-\\d{{4}}$)} expression. See tips about
defining a regular expression.

required {name:required} Rick Used to enforce that a non-


parameter value is present
during URL generation

2 Warning

When using System.Text.RegularExpressions to process untrusted input, pass a


timeout. A malicious user can provide input to RegularExpressions causing a
Denial-of-Service attack . ASP.NET Core framework APIs that use
RegularExpressions pass a timeout.

Multiple, colon delimited constraints can be applied to a single parameter. For example,
the following constraint restricts a parameter to an integer value of 1 or greater:

C#

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

2 Warning
Route constraints that verify the URL and are converted to a CLR type always use
the invariant culture. For example, conversion to the CLR type int or DateTime .
These constraints assume that the URL is not localizable. The framework-provided
route constraints don't modify the values stored in route values. All route values
parsed from the URL are stored as strings. For example, the float constraint
attempts to convert the route value to a float, but the converted value is used only
to verify it can be converted to a float.

Regular expressions in constraints

2 Warning

When using System.Text.RegularExpressions to process untrusted input, pass a


timeout. A malicious user can provide input to RegularExpressions causing a
Denial-of-Service attack . ASP.NET Core framework APIs that use
RegularExpressions pass a timeout.

Regular expressions can be specified as inline constraints using the regex(...) route
constraint. Methods in the MapControllerRoute family also accept an object literal of
constraints. If that form is used, string values are interpreted as regular expressions.

The following code uses an inline regex constraint:

C#

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
() => "Inline Regex Constraint Matched");

The following code uses an object literal to specify a regex constraint:

C#

app.MapControllerRoute(
name: "people",
pattern: "people/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List" });

The ASP.NET Core framework adds RegexOptions.IgnoreCase | RegexOptions.Compiled |


RegexOptions.CultureInvariant to the regular expression constructor. See RegexOptions

for a description of these members.


Regular expressions use delimiters and tokens similar to those used by routing and the
C# language. Regular expression tokens must be escaped. To use the regular expression
^\d{3}-\d{2}-\d{4}$ in an inline constraint, use one of the following:

Replace \ characters provided in the string as \\ characters in the C# source file


in order to escape the \ string escape character.
Verbatim string literals.

To escape routing parameter delimiter characters { , } , [ , ] , double the characters in


the expression, for example, {{ , }} , [[ , ]] . The following table shows a regular
expression and its escaped version:

Regular expression Escaped regular expression

^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$

^[a-z]{2}$ ^[[a-z]]{{2}}$

Regular expressions used in routing often start with the ^ character and match the
starting position of the string. The expressions often end with the $ character and
match the end of the string. The ^ and $ characters ensure that the regular expression
matches the entire route parameter value. Without the ^ and $ characters, the regular
expression matches any substring within the string, which is often undesirable. The
following table provides examples and explains why they match or fail to match:

Expression String Match Comment

[a-z]{2} hello Yes Substring matches

[a-z]{2} 123abc456 Yes Substring matches

[a-z]{2} mz Yes Matches expression

[a-z]{2} MZ Yes Not case sensitive

^[a-z]{2}$ hello No See ^ and $ above

^[a-z]{2}$ 123abc456 No See ^ and $ above

For more information on regular expression syntax, see .NET Framework Regular
Expressions.

To constrain a parameter to a known set of possible values, use a regular expression. For
example, {action:regex(^(list|get|create)$)} only matches the action route value to
list , get , or create . If passed into the constraints dictionary, the string
^(list|get|create)$ is equivalent. Constraints that are passed in the constraints

dictionary that don't match one of the known constraints are also treated as regular
expressions. Constraints that are passed within a template that don't match one of the
known constraints are not treated as regular expressions.

Custom route constraints


Custom route constraints can be created by implementing the IRouteConstraint
interface. The IRouteConstraint interface contains Match, which returns true if the
constraint is satisfied and false otherwise.

Custom route constraints are rarely needed. Before implementing a custom route
constraint, consider alternatives, such as model binding.

The ASP.NET Core Constraints folder provides good examples of creating constraints.
For example, GuidRouteConstraint .

To use a custom IRouteConstraint , the route constraint type must be registered with
the app's ConstraintMap in the service container. A ConstraintMap is a dictionary that
maps route constraint keys to IRouteConstraint implementations that validate those
constraints. An app's ConstraintMap can be updated in Program.cs either as part of an
AddRouting call or by configuring RouteOptions directly with
builder.Services.Configure<RouteOptions> . For example:

C#

builder.Services.AddRouting(options =>
options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

The preceding constraint is applied in the following code:

C#

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
[HttpGet("{id:noZeroes}")]
public IActionResult Get(string id) =>
Content(id);
}

The implementation of NoZeroesRouteConstraint prevents 0 being used in a route


parameter:
C#

public class NoZeroesRouteConstraint : IRouteConstraint


{
private static readonly Regex _regex = new(
@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));

public bool Match(


HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}

var routeValueString = Convert.ToString(routeValue,


CultureInfo.InvariantCulture);

if (routeValueString is null)
{
return false;
}

return _regex.IsMatch(routeValueString);
}
}

2 Warning

When using System.Text.RegularExpressions to process untrusted input, pass a


timeout. A malicious user can provide input to RegularExpressions causing a
Denial-of-Service attack . ASP.NET Core framework APIs that use
RegularExpressions pass a timeout.

The preceding code:

Prevents 0 in the {id} segment of the route.


Is shown to provide a basic example of implementing a custom constraint. It
should not be used in a production app.

The following code is a better approach to preventing an id containing a 0 from being


processed:

C#
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}

return Content(id);
}

The preceding code has the following advantages over the NoZeroesRouteConstraint
approach:

It doesn't require a custom constraint.


It returns a more descriptive error when the route parameter includes 0 .

Parameter transformers
Parameter transformers:

Execute when generating a link using LinkGenerator.


Implement Microsoft.AspNetCore.Routing.IOutboundParameterTransformer.
Are configured using ConstraintMap.
Take the parameter's route value and transform it to a new string value.
Result in using the transformed value in the generated link.

For example, a custom slugify parameter transformer in route pattern blog\


{article:slugify} with Url.Action(new { article = "MyTestArticle" }) generates
blog\my-test-article .

Consider the following IOutboundParameterTransformer implementation:

C#

public class SlugifyParameterTransformer : IOutboundParameterTransformer


{
public string? TransformOutbound(object? value)
{
if (value is null)
{
return null;
}

return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100))
.ToLowerInvariant();
}
}

To use a parameter transformer in a route pattern, configure it using ConstraintMap in


Program.cs :

C#

builder.Services.AddRouting(options =>
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

The ASP.NET Core framework uses parameter transformers to transform the URI where
an endpoint resolves. For example, parameter transformers transform the route values
used to match an area , controller , action , and page :

C#

app.MapControllerRoute(
name: "default",
pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

With the preceding route template, the action


SubscriptionManagementController.GetAll is matched with the URI /subscription-

management/get-all . A parameter transformer doesn't change the route values used to

generate a link. For example, Url.Action("GetAll", "SubscriptionManagement") outputs


/subscription-management/get-all .

ASP.NET Core provides API conventions for using parameter transformers with
generated routes:

The
Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention
MVC convention applies a specified parameter transformer to all attribute routes in
the app. The parameter transformer transforms attribute route tokens as they are
replaced. For more information, see Use a parameter transformer to customize
token replacement.
Razor Pages uses the PageRouteTransformerConvention API convention. This
convention applies a specified parameter transformer to all automatically
discovered Razor Pages. The parameter transformer transforms the folder and file
name segments of Razor Pages routes. For more information, see Use a parameter
transformer to customize page routes.

URL generation reference


This section contains a reference for the algorithm implemented by URL generation. In
practice, most complex examples of URL generation use controllers or Razor Pages. See
routing in controllers for additional information.

The URL generation process begins with a call to LinkGenerator.GetPathByAddress or a


similar method. The method is provided with an address, a set of route values, and
optionally information about the current request from HttpContext .

The first step is to use the address to resolve a set of candidate endpoints using an
IEndpointAddressScheme<TAddress> that matches the address's type.

Once the set of candidates is found by the address scheme, the endpoints are ordered
and processed iteratively until a URL generation operation succeeds. URL generation
does not check for ambiguities, the first result returned is the final result.

Troubleshooting URL generation with logging


The first step in troubleshooting URL generation is setting the logging level of
Microsoft.AspNetCore.Routing to TRACE . LinkGenerator logs many details about its
processing which can be useful to troubleshoot problems.

See URL generation reference for details on URL generation.

Addresses
Addresses are the concept in URL generation used to bind a call into the link generator
to a set of candidate endpoints.

Addresses are an extensible concept that come with two implementations by default:

Using endpoint name ( string ) as the address:


Provides similar functionality to MVC's route name.
Uses the IEndpointNameMetadata metadata type.
Resolves the provided string against the metadata of all registered endpoints.
Throws an exception on startup if multiple endpoints use the same name.
Recommended for general-purpose use outside of controllers and Razor Pages.
Using route values (RouteValuesAddress) as the address:
Provides similar functionality to controllers and Razor Pages legacy URL
generation.
Very complex to extend and debug.
Provides the implementation used by IUrlHelper , Tag Helpers, HTML Helpers,
Action Results, etc.

The role of the address scheme is to make the association between the address and
matching endpoints by arbitrary criteria:

The endpoint name scheme performs a basic dictionary lookup.


The route values scheme has a complex best subset of set algorithm.

Ambient values and explicit values


From the current request, routing accesses the route values of the current request
HttpContext.Request.RouteValues . The values associated with the current request are
referred to as the ambient values. For the purpose of clarity, the documentation refers
to the route values passed in to methods as explicit values.

The following example shows ambient values and explicit values. It provides ambient
values from the current request and explicit values:

C#

public class WidgetController : ControllerBase


{
private readonly LinkGenerator _linkGenerator;

public WidgetController(LinkGenerator linkGenerator) =>


_linkGenerator = linkGenerator;

public IActionResult Index()


{
var indexPath = _linkGenerator.GetPathByAction(
HttpContext, values: new { id = 17 })!;

return Content(indexPath);
}

// ...

The preceding code:

Returns /Widget/Index/17
Gets LinkGenerator via DI.
The following code provides only explicit values and no ambient values:

C#

var subscribePath = _linkGenerator.GetPathByAction(


"Subscribe", "Home", new { id = 17 })!;

The preceding method returns /Home/Subscribe/17

The following code in the WidgetController returns /Widget/Subscribe/17 :

C#

var subscribePath = _linkGenerator.GetPathByAction(


HttpContext, "Subscribe", null, new { id = 17 });

The following code provides the controller from ambient values in the current request
and explicit values:

C#

public class GadgetController : ControllerBase


{
public IActionResult Index() =>
Content(Url.Action("Edit", new { id = 17 })!);
}

In the preceding code:

/Gadget/Edit/17 is returned.

Url gets the IUrlHelper.


Action generates a URL with an absolute path for an action method. The URL
contains the specified action name and route values.

The following code provides ambient values from the current request and explicit
values:

C#

public class IndexModel : PageModel


{
public void OnGet()
{
var editUrl = Url.Page("./Edit", new { id = 17 });

// ...
}
}

The preceding code sets url to /Edit/17 when the Edit Razor Page contains the
following page directive:

@page "{id:int}"

If the Edit page doesn't contain the "{id:int}" route template, url is /Edit?id=17 .

The behavior of MVC's IUrlHelper adds a layer of complexity in addition to the rules
described here:

IUrlHelper always provides the route values from the current request as ambient
values.
IUrlHelper.Action always copies the current action and controller route values as
explicit values unless overridden by the developer.
IUrlHelper.Page always copies the current page route value as an explicit value
unless overridden.
IUrlHelper.Page always overrides the current handler route value with null as an

explicit values unless overridden.

Users are often surprised by the behavioral details of ambient values, because MVC
doesn't seem to follow its own rules. For historical and compatibility reasons, certain
route values such as action , controller , page , and handler have their own special-case
behavior.

The equivalent functionality provided by LinkGenerator.GetPathByAction and


LinkGenerator.GetPathByPage duplicates these anomalies of IUrlHelper for

compatibility.

URL generation process


Once the set of candidate endpoints are found, the URL generation algorithm:

Processes the endpoints iteratively.


Returns the first successful result.

The first step in this process is called route value invalidation. Route value invalidation is
the process by which routing decides which route values from the ambient values
should be used and which should be ignored. Each ambient value is considered and
either combined with the explicit values, or ignored.
The best way to think about the role of ambient values is that they attempt to save
application developers typing, in some common cases. Traditionally, the scenarios where
ambient values are helpful are related to MVC:

When linking to another action in the same controller, the controller name doesn't
need to be specified.
When linking to another controller in the same area, the area name doesn't need
to be specified.
When linking to the same action method, route values don't need to be specified.
When linking to another part of the app, you don't want to carry over route values
that have no meaning in that part of the app.

Calls to LinkGenerator or IUrlHelper that return null are usually caused by not
understanding route value invalidation. Troubleshoot route value invalidation by
explicitly specifying more of the route values to see if that solves the problem.

Route value invalidation works on the assumption that the app's URL scheme is
hierarchical, with a hierarchy formed from left-to-right. Consider the basic controller
route template {controller}/{action}/{id?} to get an intuitive sense of how this works
in practice. A change to a value invalidates all of the route values that appear to the
right. This reflects the assumption about hierarchy. If the app has an ambient value for
id , and the operation specifies a different value for the controller :

id won't be reused because {controller} is to the left of {id?} .

Some examples demonstrating this principle:

If the explicit values contain a value for id , the ambient value for id is ignored.
The ambient values for controller and action can be used.
If the explicit values contain a value for action , any ambient value for action is
ignored. The ambient values for controller can be used. If the explicit value for
action is different from the ambient value for action , the id value won't be used.
If the explicit value for action is the same as the ambient value for action , the id
value can be used.
If the explicit values contain a value for controller , any ambient value for
controller is ignored. If the explicit value for controller is different from the

ambient value for controller , the action and id values won't be used. If the
explicit value for controller is the same as the ambient value for controller , the
action and id values can be used.

This process is further complicated by the existence of attribute routes and dedicated
conventional routes. Controller conventional routes such as
{controller}/{action}/{id?} specify a hierarchy using route parameters. For dedicated

conventional routes and attribute routes to controllers and Razor Pages:

There is a hierarchy of route values.


They don't appear in the template.

For these cases, URL generation defines the required values concept. Endpoints created
by controllers and Razor Pages have required values specified that allow route value
invalidation to work.

The route value invalidation algorithm in detail:

The required value names are combined with the route parameters, then
processed from left-to-right.
For each parameter, the ambient value and explicit value are compared:
If the ambient value and explicit value are the same, the process continues.
If the ambient value is present and the explicit value isn't, the ambient value is
used when generating the URL.
If the ambient value isn't present and the explicit value is, reject the ambient
value and all subsequent ambient values.
If the ambient value and the explicit value are present, and the two values are
different, reject the ambient value and all subsequent ambient values.

At this point, the URL generation operation is ready to evaluate route constraints. The
set of accepted values is combined with the parameter default values, which is provided
to constraints. If the constraints all pass, the operation continues.

Next, the accepted values can be used to expand the route template. The route
template is processed:

From left-to-right.
Each parameter has its accepted value substituted.
With the following special cases:
If the accepted values is missing a value and the parameter has a default value,
the default value is used.
If the accepted values is missing a value and the parameter is optional,
processing continues.
If any route parameter to the right of a missing optional parameter has a value,
the operation fails.
Contiguous default-valued parameters and optional parameters are collapsed
where possible.
Values explicitly provided that don't match a segment of the route are added to the
query string. The following table shows the result when using the route template
{controller}/{action}/{id?} .

Ambient Values Explicit Values Result

controller = "Home" action = "About" /Home/About

controller = "Home" controller = "Order", action = /Order/About


"About"

controller = "Home", color = action = "About" /Home/About


"Red"

controller = "Home" action = "About", color = "Red" /Home/About?


color=Red

Optional route parameter order


Optional route parameters must come after all required route parameters. In the
following code, the id and name parameters must come after the color parameter:

C#

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
// GET /api/my/red/2/joe
// GET /api/my/red/2
// GET /api/my
[HttpGet("{color}/{id:int?}/{name?}")]
public IActionResult GetByIdAndOptionalName(string color, int id = 1,
string? name = null)
{
return Ok($"{color} {id} {name ?? ""}");
}
}

Problems with route value invalidation


The following code shows an example of a URL generation scheme that's not supported
by routing:
C#

app.MapControllerRoute(
"default",
"{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
"blog",
"{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost" });

In the preceding code, the culture route parameter is used for localization. The desire is
to have the culture parameter always accepted as an ambient value. However, the
culture parameter is not accepted as an ambient value because of the way required
values work:

In the "default" route template, the culture route parameter is to the left of
controller , so changes to controller won't invalidate culture .

In the "blog" route template, the culture route parameter is considered to be to


the right of controller , which appears in the required values.

Parse URL paths with LinkParser


The LinkParser class adds support for parsing a URL path into a set of route values. The
ParsePathByEndpointName method takes an endpoint name and a URL path, and
returns a set of route values extracted from the URL path.

In the following example controller, the GetProduct action uses a route template of
api/Products/{id} and has a Name of GetProduct :

C#

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}", Name = nameof(GetProduct))]
public IActionResult GetProduct(string id)
{
// ...

In the same controller class, the AddRelatedProduct action expects a URL path,
pathToRelatedProduct , which can be provided as a query-string parameter:
C#

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
string id, string pathToRelatedProduct, [FromServices] LinkParser
linkParser)
{
var routeValues = linkParser.ParsePathByEndpointName(
nameof(GetProduct), pathToRelatedProduct);
var relatedProductId = routeValues?["id"];

// ...

In the preceding example, the AddRelatedProduct action extracts the id route value
from the URL path. For example, with a URL path of /api/Products/1 , the
relatedProductId value is set to 1 . This approach allows the API's clients to use URL
paths when referring to resources, without requiring knowledge of how such a URL is
structured.

Configure endpoint metadata


The following links provide information on how to configure endpoint metadata:

Enable Cors with endpoint routing


IAuthorizationPolicyProvider sample using a custom [MinimumAgeAuthorize]
attribute
Test authentication with the [Authorize] attribute
RequireAuthorization
Selecting the scheme with the [Authorize] attribute
Apply policies using the [Authorize] attribute
Role-based authorization in ASP.NET Core

Host matching in routes with RequireHost


RequireHost applies a constraint to the route which requires the specified host. The
RequireHost or [Host] parameter can be a:

Host: www.domain.com , matches www.domain.com with any port.


Host with wildcard: *.domain.com , matches www.domain.com , subdomain.domain.com ,
or www.subdomain.domain.com on any port.
Port: *:5000 , matches port 5000 with any host.
Host and port: www.domain.com:5000 or *.domain.com:5000 , matches host and port.
Multiple parameters can be specified using RequireHost or [Host] . The constraint
matches hosts valid for any of the parameters. For example, [Host("domain.com",
"*.domain.com")] matches domain.com , www.domain.com , and subdomain.domain.com .

The following code uses RequireHost to require the specified host on the route:

C#

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");


app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

The following code uses the [Host] attribute on the controller to require any of the
specified hosts:

C#

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
public IActionResult Index() =>
View();

[Host("example.com")]
public IActionResult Example() =>
View();
}

When the [Host] attribute is applied to both the controller and action method:

The attribute on the action is used.


The controller attribute is ignored.

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

Performance guidance for routing


When an app has performance problems, routing is often suspected as the problem.
The reason routing is suspected is that frameworks like controllers and Razor Pages
report the amount of time spent inside the framework in their logging messages. When
there's a significant difference between the time reported by controllers and the total
time of the request:

Developers eliminate their app code as the source of the problem.


It's common to assume routing is the cause.

Routing is performance tested using thousands of endpoints. It's unlikely that a typical
app will encounter a performance problem just by being too large. The most common
root cause of slow routing performance is usually a badly-behaving custom middleware.

This following code sample demonstrates a basic technique for narrowing down the
source of delay:

C#

var logger = app.Services.GetRequiredService<ILogger<Program>>();

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


{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();

logger.LogInformation("Time 1: {ElapsedMilliseconds}ms",
stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

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


{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();

logger.LogInformation("Time 2: {ElapsedMilliseconds}ms",
stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

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


{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();

logger.LogInformation("Time 3: {ElapsedMilliseconds}ms",
stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");


To time routing:

Interleave each middleware with a copy of the timing middleware shown in the
preceding code.
Add a unique identifier to correlate the timing data with the code.

This is a basic way to narrow down the delay when it's significant, for example, more
than 10ms . Subtracting Time 2 from Time 1 reports the time spent inside the
UseRouting middleware.

The following code uses a more compact approach to the preceding timing code:

C#

public sealed class AutoStopwatch : IDisposable


{
private readonly ILogger _logger;
private readonly string _message;
private readonly Stopwatch _stopwatch;
private bool _disposed;

public AutoStopwatch(ILogger logger, string message) =>


(_logger, _message, _stopwatch) = (logger, message,
Stopwatch.StartNew());

public void Dispose()


{
if (_disposed)
{
return;
}

_logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
_message, _stopwatch.ElapsedMilliseconds);

_disposed = true;
}
}

C#

var logger = app.Services.GetRequiredService<ILogger<Program>>();


var timerCount = 0;

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


{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});

app.UseRouting();

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


{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});

app.UseAuthorization();

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


{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});

app.MapGet("/", () => "Timing Test.");

Potentially expensive routing features


The following list provides some insight into routing features that are relatively
expensive compared with basic route templates:

Regular expressions: It's possible to write regular expressions that are complex, or
have long running time with a small amount of input.
Complex segments ( {x}-{y}-{z} ):
Are significantly more expensive than parsing a regular URL path segment.
Result in many more substrings being allocated.
Synchronous data access: Many complex apps have database access as part of
their routing. Use extensibility points such as MatcherPolicy and
EndpointSelectorContext, which are asynchronous.

Guidance for large route tables


By default ASP.NET Core uses a routing algorithm that trades memory for CPU time. This
has the nice effect that route matching time is dependent only on the length of the path
to match and not the number of routes. However, this approach can be potentially
problematic in some cases, when the app has a large number of routes (in the
thousands) and there is a high amount of variable prefixes in the routes. For example, if
the routes have parameters in early segments of the route, like
{parameter}/some/literal .

It is unlikely for an app to run into a situation where this is a problem unless:

There are a high number of routes in the app using this pattern.
There is a large number of routes in the app.

How to determine if an app is running into the large route table


problem

There are two symptoms to look for:


The app is slow to start on the first request.
Note that this is required but not sufficient. There are many other non-route
problems than can cause slow app startup. Check for the condition below to
accurately determine the app is running into this situation.
The app consumes a lot of memory during startup and a memory dump shows
a large number of Microsoft.AspNetCore.Routing.Matching.DfaNode instances.

How to address this issue


There are several techniques and optimizations can be applied to routes that will largely
improve this scenario:

Apply route constraints to your parameters, for example {parameter:int} ,


{parameter:guid} , {parameter:regex(\\d+)} , etc. where possible.

This allows the routing algorithm to internally optimize the structures used for
matching and drastically reduce the memory used.
In the vast majority of cases this will suffice to get back to an acceptable
behavior.
Change the routes to move parameters to later segments in the template.
This reduces the number of possible "paths" to match an endpoint given a path.
Use a dynamic route and perform the mapping to a controller/page dynamically.
This can be achieved using MapDynamicControllerRoute and
MapDynamicPageRoute .

Guidance for library authors


This section contains guidance for library authors building on top of routing. These
details are intended to ensure that app developers have a good experience using
libraries and frameworks that extend routing.
Define endpoints
To create a framework that uses routing for URL matching, start by defining a user
experience that builds on top of UseEndpoints.

DO build on top of IEndpointRouteBuilder. This allows users to compose your


framework with other ASP.NET Core features without confusion. Every ASP.NET Core
template includes routing. Assume routing is present and familiar for users.

C#

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

DO return a sealed concrete type from a call to MapMyFramework(...) that implements


IEndpointConventionBuilder. Most framework Map... methods follow this pattern. The
IEndpointConventionBuilder interface:

Allows for metadata to be composed.


Is targeted by a variety of extension methods.

Declaring your own type allows you to add your own framework-specific functionality to
the builder. It's ok to wrap a framework-declared builder and forward calls to it.

C#

// Your framework
app.MapMyFramework(...)
.RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

CONSIDER writing your own EndpointDataSource. EndpointDataSource is the low-level


primitive for declaring and updating a collection of endpoints. EndpointDataSource is a
powerful API used by controllers and Razor Pages.

The routing tests have a basic example of a non-updating data source.

CONSIDER implementing GetGroupedEndpoints. This gives complete control over


running group conventions and the final metadata on the grouped endpoints. For
example, this allows custom EndpointDataSource implementations to run endpoint filters
added to groups.
DO NOT attempt to register an EndpointDataSource by default. Require users to register
your framework in UseEndpoints. The philosophy of routing is that nothing is included
by default, and that UseEndpoints is the place to register endpoints.

Creating routing-integrated middleware


CONSIDER defining metadata types as an interface.

DO make it possible to use metadata types as an attribute on classes and methods.

C#

public interface ICoolMetadata


{
bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}

Frameworks like controllers and Razor Pages support applying metadata attributes to
types and methods. If you declare metadata types:

Make them accessible as attributes.


Most users are familiar with applying attributes.

Declaring a metadata type as an interface adds another layer of flexibility:

Interfaces are composable.


Developers can declare their own types that combine multiple policies.

DO make it possible to override metadata, as shown in the following example:

C#

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}

The best way to follow these guidelines is to avoid defining marker metadata:

Don't just look for the presence of a metadata type.


Define a property on the metadata and check the property.

The metadata collection is ordered and supports overriding by priority. In the case of
controllers, metadata on the action method is most specific.

DO make middleware useful with and without routing:

C#

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

As an example of this guideline, consider the UseAuthorization middleware. The


authorization middleware allows you to pass in a fallback policy. The fallback policy, if
specified, applies to both:

Endpoints without a specified policy.


Requests that don't match an endpoint.

This makes the authorization middleware useful outside of the context of routing. The
authorization middleware can be used for traditional middleware programming.

Debug diagnostics
For detailed routing diagnostic output, set Logging:LogLevel:Microsoft to Debug . In the
development environment, set the log level in appsettings.Development.json :

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

Additional resources
View or download sample code (how to download)
Handle errors in ASP.NET Core
Article • 04/11/2023

By Tom Dykstra

This article covers common approaches to handling errors in ASP.NET Core web apps.
See Handle errors in ASP.NET Core web APIs for web APIs.

Developer exception page


The Developer Exception Page displays detailed information about unhandled request
exceptions. ASP.NET Core apps enable the developer exception page by default when
both:

Running in the Development environment.


App created with the current templates, that is, using
WebApplication.CreateBuilder. Apps created using the
WebHost.CreateDefaultBuilder must enable the developer exception page by
calling app.UseDeveloperExceptionPage in Configure .

The developer exception page runs early in the middleware pipeline, so that it can catch
unhandled exceptions thrown in middleware that follows.

Detailed exception information shouldn't be displayed publicly when the app runs in the
Production environment. For more information on configuring environments, see Use
multiple environments in ASP.NET Core.

The Developer Exception Page can include the following information about the
exception and the request:

Stack trace
Query string parameters, if any
Cookies, if any
Headers

The Developer Exception Page isn't guaranteed to provide any information. Use Logging
for complete error information.

Exception handler page


To configure a custom error handling page for the Production environment, call
UseExceptionHandler. This exception handling middleware:

Catches and logs unhandled exceptions.


Re-executes the request in an alternate pipeline using the path indicated. The
request isn't re-executed if the response has started. The template-generated code
re-executes the request using the /Error path.

2 Warning

If the alternate pipeline throws an exception of its own, Exception Handling


Middleware rethrows the original exception.

Since this middleware can re-execute the request pipeline:

Middlewares need to handle reentrancy with the same request. This normally
means either cleaning up their state after calling _next or caching their processing
on the HttpContext to avoid redoing it. When dealing with the request body, this
either means buffering or caching the results like the Form reader.
For the UseExceptionHandler(IApplicationBuilder, String) overload that is used in
templates, only the request path is modified, and the route data is cleared. Request
data such as headers, method, and items are all reused as-is.
Scoped services remain the same.

In the following example, UseExceptionHandler adds the exception handling middleware


in non-Development environments:

C#

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

The Razor Pages app template provides an Error page ( .cshtml ) and PageModel class
( ErrorModel ) in the Pages folder. For an MVC app, the project template includes an
Error action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the original HTTP
method. If an error handler endpoint is restricted to a specific set of HTTP methods, it
runs only for those HTTP methods. For example, an MVC controller action that uses the
[HttpGet] attribute runs only for GET requests. To ensure that all requests reach the
custom error handling page, don't restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

For Razor Pages, create multiple handler methods. For example, use OnGet to
handle GET exceptions and use OnPost to handle POST exceptions.
For MVC, apply HTTP verb attributes to multiple actions. For example, use
[HttpGet] to handle GET exceptions and use [HttpPost] to handle POST

exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it
supports anonymous access.

Access the exception


Use IExceptionHandlerPathFeature to access the exception and the original request path
in an error handler. The following example uses IExceptionHandlerPathFeature to get
more information about the exception that was thrown:

C#

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore


= true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

public string? ExceptionMessage { get; set; }

public void OnGet()


{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();

if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "The file was not found.";
}

if (exceptionHandlerPathFeature?.Path == "/")
{
ExceptionMessage ??= string.Empty;
ExceptionMessage += " Page: Home.";
}
}
}

2 Warning

Do not serve sensitive error information to clients. Serving errors is a security risk.

Exception handler lambda


An alternative to a custom exception handler page is to provide a lambda to
UseExceptionHandler. Using a lambda allows access to the error before returning the
response.

The following code uses a lambda for exception handling:

C#

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode =
StatusCodes.Status500InternalServerError;

// using static System.Net.Mime.MediaTypeNames;


context.Response.ContentType = Text.Plain;

await context.Response.WriteAsync("An exception was thrown.");

var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();

if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
await context.Response.WriteAsync(" The file was not
found.");
}

if (exceptionHandlerPathFeature?.Path == "/")
{
await context.Response.WriteAsync(" Page: Home.");
}
});
});

app.UseHsts();
}

2 Warning

Do not serve sensitive error information to clients. Serving errors is a security risk.

UseStatusCodePages
By default, an ASP.NET Core app doesn't provide a status code page for HTTP error
status codes, such as 404 - Not Found. When the app sets an HTTP 400-599 error status
code that doesn't have a body, it returns the status code and an empty response body.
To enable default text-only handlers for common error status codes, call
UseStatusCodePages in Program.cs :

C#

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseStatusCodePages();

Call UseStatusCodePages before request handling middleware. For example, call


UseStatusCodePages before the Static File Middleware and the Endpoints Middleware.

When UseStatusCodePages isn't used, navigating to a URL without an endpoint returns a


browser-dependent error message indicating the endpoint can't be found. When
UseStatusCodePages is called, the browser returns the following response:

Console

Status Code: 404; Not Found

UseStatusCodePages isn't typically used in production because it returns a message that


isn't useful to users.

7 Note

The status code pages middleware does not catch exceptions. To provide a custom
error handling page, use the exception handler page.

UseStatusCodePages with format string


To customize the response content type and text, use the overload of
UseStatusCodePages that takes a content type and format string:

C#

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;


app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
In the preceding code, {0} is a placeholder for the error code.

UseStatusCodePages with a format string isn't typically used in production because it


returns a message that isn't useful to users.

UseStatusCodePages with lambda


To specify custom error-handling and response-writing code, use the overload of
UseStatusCodePages that takes a lambda expression:

C#

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>


{
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page:
{statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages with a lambda isn't typically used in production because it returns a


message that isn't useful to users.

UseStatusCodePagesWithRedirects
The UseStatusCodePagesWithRedirects extension method:

Sends a 302 - Found status code to the client.


Redirects the client to the error handling endpoint provided in the URL template.
The error handling endpoint typically displays error information and returns HTTP
200.

C#

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

The URL template can include a {0} placeholder for the status code, as shown in the
preceding code. If the URL template starts with ~ (tilde), the ~ is replaced by the app's
PathBase . When specifying an endpoint in the app, create an MVC view or Razor page

for the endpoint.

This method is commonly used when the app:

Should redirect the client to a different endpoint, usually in cases where a different
app processes the error. For web apps, the client's browser address bar reflects the
redirected endpoint.
Shouldn't preserve and return the original status code with the initial redirect
response.

UseStatusCodePagesWithReExecute
The UseStatusCodePagesWithReExecute extension method:

Generates the response body by re-executing the request pipeline using an


alternate path.
Does not alter the status code before or after re-executing the pipeline.

The new pipeline execution may alter the response's status code, as the new pipeline
has full control of the status code. If the new pipeline does not alter the status code, the
original status code will be sent to the client.

C#

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
If an endpoint within the app is specified, create an MVC view or Razor page for the
endpoint.

This method is commonly used when the app should:

Process the request without redirecting to a different endpoint. For web apps, the
client's browser address bar reflects the originally requested endpoint.
Preserve and return the original status code with the response.

The URL template must start with / and may include a placeholder {0} for the status
code. To pass the status code as a query-string parameter, pass a second argument into
UseStatusCodePagesWithReExecute . For example:

C#

app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

The endpoint that processes the error can get the original URL that generated the error,
as shown in the following example:

C#

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore


= true)]
public class StatusCodeModel : PageModel
{
public int OriginalStatusCode { get; set; }

public string? OriginalPathAndQuery { get; set; }

public void OnGet(int statusCode)


{
OriginalStatusCode = statusCode;

var statusCodeReExecuteFeature =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

if (statusCodeReExecuteFeature is not null)


{
OriginalPathAndQuery = string.Join(
statusCodeReExecuteFeature.OriginalPathBase,
statusCodeReExecuteFeature.OriginalPath,
statusCodeReExecuteFeature.OriginalQueryString);
}
}
}

Since this middleware can re-execute the request pipeline:


Middlewares need to handle reentrancy with the same request. This normally
means either cleaning up their state after calling _next or caching their processing
on the HttpContext to avoid redoing it. When dealing with the request body, this
either means buffering or caching the results like the Form reader.
Scoped services remain the same.

Disable status code pages


To disable status code pages for an MVC controller or action method, use the
[SkipStatusCodePages] attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in
an MVC controller, use IStatusCodePagesFeature:

C#

public void OnGet()


{
var statusCodePagesFeature =
HttpContext.Features.Get<IStatusCodePagesFeature>();

if (statusCodePagesFeature is not null)


{
statusCodePagesFeature.Enabled = false;
}
}

Exception-handling code
Code in exception handling pages can also throw exceptions. Production error pages
should be tested thoroughly and take extra care to avoid throwing exceptions of their
own.

Response headers
Once the headers for a response are sent:

The app can't change the response's status code.


Any exception pages or handlers can't run. The response must be completed or
the connection aborted.

Server exception handling


In addition to the exception handling logic in an app, the HTTP server implementation
can handle some exceptions. If the server catches an exception before response headers
are sent, the server sends a 500 - Internal Server Error response without a response
body. If the server catches an exception after response headers are sent, the server
closes the connection. Requests that aren't handled by the app are handled by the
server. Any exception that occurs when the server is handling the request is handled by
the server's exception handling. The app's custom error pages, exception handling
middleware, and filters don't affect this behavior.

Startup exception handling


Only the hosting layer can handle exceptions that take place during app startup. The
host can be configured to capture startup errors and capture detailed errors.

The hosting layer can show an error page for a captured startup error only if the error
occurs after host address/port binding. If binding fails:

The hosting layer logs a critical exception.


The dotnet process crashes.
No error page is displayed when the HTTP server is Kestrel.

When running on IIS (or Azure App Service) or IIS Express, a 502.5 - Process Failure is
returned by the ASP.NET Core Module if the process can't start. For more information,
see Troubleshoot ASP.NET Core on Azure App Service and IIS.

Database error page


The Database developer page exception filter
AddDatabaseDeveloperPageExceptionFilter captures database-related exceptions that
can be resolved by using Entity Framework Core migrations. When these exceptions
occur, an HTML response is generated with details of possible actions to resolve the
issue. This page is enabled only in the Development environment. The following code
adds the Database developer page exception filter:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
Exception filters
In MVC apps, exception filters can be configured globally or on a per-controller or per-
action basis. In Razor Pages apps, they can be configured globally or per page model.
These filters handle any unhandled exceptions that occur during the execution of a
controller action or another filter. For more information, see Filters in ASP.NET Core.

Exception filters are useful for trapping exceptions that occur within MVC actions, but
they're not as flexible as the built-in exception handling middleware ,
UseExceptionHandler. We recommend using UseExceptionHandler , unless you need to
perform error handling differently based on which MVC action is chosen.

Model state errors


For information about how to handle model state errors, see Model binding and Model
validation.

Problem details
Problem Details are not the only response format to describe an HTTP API error,
however, they are commonly used to report errors for HTTP APIs.

The problem details service implements the IProblemDetailsService interface, which


supports creating problem details in ASP.NET Core. The AddProblemDetails extension
method on IServiceCollection registers the default IProblemDetailsService
implementation.

In ASP.NET Core apps, the following middleware generates problem details HTTP
responses when AddProblemDetails is called, except when the Accept request HTTP
header doesn't include one of the content types supported by the registered
IProblemDetailsWriter (default: application/json ):

ExceptionHandlerMiddleware: Generates a problem details response when a


custom handler is not defined.
StatusCodePagesMiddleware: Generates a problem details response by default.
DeveloperExceptionPageMiddleware: Generates a problem details response in
development when the Accept request HTTP header does not include text/html .

The following code configures the app to generate a problem details response for all
HTTP client and server error responses that don't have a body content yet:

C#
var app = builder.Build();

builder.Services.AddProblemDetails();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}

app.UseStatusCodePages();

The next section shows how to customize the problem details response body.

Customize problem details


The automatic creation of a ProblemDetails can be customized using any of the
following options:

1. Use ProblemDetailsOptions.CustomizeProblemDetails
2. Use a custom IProblemDetailsWriter
3. Call the IProblemDetailsService in a middleware

CustomizeProblemDetails operation

The generated problem details can be customized using CustomizeProblemDetails, and


the customizations are applied to all auto-generated problem details.

The following code uses ProblemDetailsOptions to set CustomizeProblemDetails:

C#

var app = builder.Build();

builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx =>
ctx.ProblemDetails.Extensions.Add("nodeId",
Environment.MachineName));

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}

app.UseStatusCodePages();
For example, an HTTP Status 400 Bad Request endpoint result produces the following
problem details response body:

JSON

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"nodeId": "my-machine-name"
}

Custom IProblemDetailsWriter

An IProblemDetailsWriter implementation can be created for advanced customizations.

C#

public class SampleProblemDetailsWriter : IProblemDetailsWriter


{
// Indicates that only responses with StatusCode == 400
// are handled by this writer. All others are
// handled by different registered writers if available.
public bool CanWrite(ProblemDetailsContext context)
=> context.HttpContext.Response.StatusCode == 400;

public ValueTask WriteAsync(ProblemDetailsContext context)


{
// Additional customizations.

// Write to the response.


var response = context.HttpContext.Response;
return new
ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
}
}

Note: When using a custom IProblemDetailsWriter , the custom IProblemDetailsWriter


must be registered before calling AddRazorPages, AddControllers, or
AddControllersWithViews:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter,
SampleProblemDetailsWriter>();
var app = builder.Build();

// Middleware to handle writing problem details to the response.


app.Use(async (context, next) =>
{
await next(context);
var mathErrorFeature = context.Features.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
if (context.RequestServices.GetService<IProblemDetailsWriter>() is
{ } problemDetailsService)
{

if (problemDetailsService.CanWrite(new ProblemDetailsContext() {
HttpContext = context }))
{
(string Detail, string Type) details =
mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError => ("Divison by zero
is not defined.",
"https://en.wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid
input.",
"https://en.wikipedia.org/wiki/Square_root")
};

await problemDetailsService.WriteAsync(new
ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = "Bad Input",
Detail = details.Detail,
Type = details.Type
}
});
}
}
}
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double
denominator) =>
{
if (denominator == 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.DivisionByZeroError
};
context.Features.Set(errorType);
return Results.BadRequest();
}

return Results.Ok(numerator / denominator);


});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
context.Features.Set(errorType);
return Results.BadRequest();
}

return Results.Ok(Math.Sqrt(radicand));
});

app.Run();

Problem details from Middleware


An alternative approach to using ProblemDetailsOptions with CustomizeProblemDetails
is to set the ProblemDetails in middleware. A problem details response can be written
by calling IProblemDetailsService.WriteAsync:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.


app.Use(async (context, next) =>
{
await next(context);
var mathErrorFeature = context.Features.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
if (context.RequestServices.GetService<IProblemDetailsService>() is
{ }
problemDetailsService)
{
(string Detail, string Type) details =
mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError => ("Divison by zero is
not defined.",
"https://en.wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid input.",
"https://en.wikipedia.org/wiki/Square_root")
};

await problemDetailsService.WriteAsync(new ProblemDetailsContext


{
HttpContext = context,
ProblemDetails =
{
Title = "Bad Input",
Detail = details.Detail,
Type = details.Type
}
});
}
}
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double
denominator) =>
{
if (denominator == 0)
{
var errorType = new MathErrorFeature { MathError =

MathErrorType.DivisionByZeroError };
context.Features.Set(errorType);
return Results.BadRequest();
}

return Results.Ok(numerator / denominator);


});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
if (radicand < 0)
{
var errorType = new MathErrorFeature { MathError =

MathErrorType.NegativeRadicandError };
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();

In the preceding code, the minimal API endpoints /divide and /squareroot return the
expected custom problem response on error input.

The API controller endpoints return the default problem response on error input, not the
custom problem response. The default problem response is returned because the API
controller has written to the response stream, Problem details for error status codes,
before IProblemDetailsService.WriteAsync is called and the response is not written
again.

The following ValuesController returns BadRequestResult, which writes to the response


stream and therefore prevents the custom problem response from being returned.

C#

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
// /api/values/divide/1/2
[HttpGet("{Numerator}/{Denominator}")]
public IActionResult Divide(double Numerator, double Denominator)
{
if (Denominator == 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.DivisionByZeroError
};
HttpContext.Features.Set(errorType);
return BadRequest();
}

return Ok(Numerator / Denominator);


}

// /api/values/squareroot/4
[HttpGet("{radicand}")]
public IActionResult Squareroot(double radicand)
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
HttpContext.Features.Set(errorType);
return BadRequest();
}
return Ok(Math.Sqrt(radicand));
}

The following Values3Controller returns ControllerBase.Problem so the expected


custom problem result is returned:

C#

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
// /api/values3/divide/1/2
[HttpGet("{Numerator}/{Denominator}")]
public IActionResult Divide(double Numerator, double Denominator)
{
if (Denominator == 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.DivisionByZeroError
};
HttpContext.Features.Set(errorType);
return Problem(
title: "Bad Input",
detail: "Divison by zero is not defined.",
type: "https://en.wikipedia.org/wiki/Division_by_zero",
statusCode: StatusCodes.Status400BadRequest
);
}

return Ok(Numerator / Denominator);


}

// /api/values3/squareroot/4
[HttpGet("{radicand}")]
public IActionResult Squareroot(double radicand)
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
HttpContext.Features.Set(errorType);
return Problem(
title: "Bad Input",
detail: "Negative or complex numbers are not valid input.",
type: "https://en.wikipedia.org/wiki/Square_root",
statusCode: StatusCodes.Status400BadRequest
);
}

return Ok(Math.Sqrt(radicand));
}

Produce a ProblemDetails payload for


exceptions
Consider the following app:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

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

app.MapControllers();
app.Run();

In non-development environments, when an exception occurs, the following is a


standard ProblemDetails response that is returned to the client:

JSON

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

For most apps, the preceding code is all that's needed for exceptions. However, the
following section shows how to get more detailed problem responses.

An alternative to a custom exception handler page is to provide a lambda to


UseExceptionHandler. Using a lambda allows access to the error and writing a problem
details response with IProblemDetailsService.WriteAsync:

C#

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode =
StatusCodes.Status500InternalServerError;
context.Response.ContentType = Text.Plain;

var title = "Bad Input";


var detail = "Invalid input";
var type = "https://errors.example.com/badInput";

if (context.RequestServices.GetService<IProblemDetailsService>()
is
{ } problemDetailsService)
{
var exceptionHandlerFeature =
context.Features.Get<IExceptionHandlerFeature>();

var exceptionType = exceptionHandlerFeature?.Error;


if (exceptionType != null &&
exceptionType.Message.Contains("infinity"))
{
title = "Arguement exception";
detail = "Invalid input";
type = "https://errors.example.com/arguementException";
}

await problemDetailsService.WriteAsync(new
ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = title,
Detail = detail,
Type = type
}
});
}
});
});
}

app.MapControllers();
app.Run();

2 Warning

Do not serve sensitive error information to clients. Serving errors is a security risk.

An alternative approach to generate problem details is to use the third-party NuGet


package Hellang.Middleware.ProblemDetails that can be used to map exceptions and
client errors to problem details.

Additional resources
View or download sample code (how to download)
Troubleshoot ASP.NET Core on Azure App Service and IIS
Common error troubleshooting for Azure App Service and IIS with ASP.NET Core
Make HTTP requests using
IHttpClientFactory in ASP.NET Core
Article • 04/11/2023

By Kirk Larkin , Steve Gordon , Glenn Condron , and Ryan Nowak .

An IHttpClientFactory can be registered and used to configure and create HttpClient


instances in an app. IHttpClientFactory offers the following benefits:

Provides a central location for naming and configuring logical HttpClient


instances. For example, a client named github could be registered and configured
to access GitHub . A default client can be registered for general access.
Codifies the concept of outgoing middleware via delegating handlers in
HttpClient . Provides extensions for Polly-based middleware to take advantage of

delegating handlers in HttpClient .


Manages the pooling and lifetime of underlying HttpClientMessageHandler
instances. Automatic management avoids common DNS (Domain Name System)
problems that occur when manually managing HttpClient lifetimes.
Adds a configurable logging experience (via ILogger ) for all requests sent through
clients created by the factory.

The sample code in this topic version uses System.Text.Json to deserialize JSON content
returned in HTTP responses. For samples that use Json.NET and ReadAsAsync<T> , use the
version selector to select a 2.x version of this topic.

Consumption patterns
There are several ways IHttpClientFactory can be used in an app:

Basic usage
Named clients
Typed clients
Generated clients

The best approach depends upon the app's requirements.

Basic usage
Register IHttpClientFactory by calling AddHttpClient in Program.cs :
C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddHttpClient();

An IHttpClientFactory can be requested using dependency injection (DI). The following


code uses IHttpClientFactory to create an HttpClient instance:

C#

public class BasicModel : PageModel


{
private readonly IHttpClientFactory _httpClientFactory;

public BasicModel(IHttpClientFactory httpClientFactory) =>


_httpClientFactory = httpClientFactory;

public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

public async Task OnGet()


{
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
{
Headers =
{
{ HeaderNames.Accept, "application/vnd.github.v3+json" },
{ HeaderNames.UserAgent, "HttpRequestsSample" }
}
};

var httpClient = _httpClientFactory.CreateClient();


var httpResponseMessage = await
httpClient.SendAsync(httpRequestMessage);

if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();

GitHubBranches = await JsonSerializer.DeserializeAsync


<IEnumerable<GitHubBranch>>(contentStream);
}
}
}

Using IHttpClientFactory like in the preceding example is a good way to refactor an


existing app. It has no impact on how HttpClient is used. In places where HttpClient
instances are created in an existing app, replace those occurrences with calls to
CreateClient.

Named clients
Named clients are a good choice when:

The app requires many distinct uses of HttpClient .


Many HttpClient s have different configuration.

Specify configuration for a named HttpClient during its registration in Program.cs :

C#

builder.Services.AddHttpClient("GitHub", httpClient =>


{
httpClient.BaseAddress = new Uri("https://api.github.com/");

// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});

In the preceding code the client is configured with:

The base address https://api.github.com/ .


Two headers required to work with the GitHub API.

CreateClient

Each time CreateClient is called:

A new instance of HttpClient is created.


The configuration action is called.

To create a named client, pass its name into CreateClient :

C#

public class NamedClientModel : PageModel


{
private readonly IHttpClientFactory _httpClientFactory;
public NamedClientModel(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;

public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

public async Task OnGet()


{
var httpClient = _httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");

if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();

GitHubBranches = await JsonSerializer.DeserializeAsync


<IEnumerable<GitHubBranch>>(contentStream);
}
}
}

In the preceding code, the request doesn't need to specify a hostname. The code can
pass just the path, since the base address configured for the client is used.

Typed clients
Typed clients:

Provide the same capabilities as named clients without the need to use strings as
keys.
Provides IntelliSense and compiler help when consuming clients.
Provide a single location to configure and interact with a particular HttpClient . For
example, a single typed client might be used:
For a single backend endpoint.
To encapsulate all logic dealing with the endpoint.
Work with DI and can be injected where required in the app.

A typed client accepts an HttpClient parameter in its constructor:

C#

public class GitHubService


{
private readonly HttpClient _httpClient;

public GitHubService(HttpClient httpClient)


{
_httpClient = httpClient;

_httpClient.BaseAddress = new Uri("https://api.github.com/");

// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
}

public async Task<IEnumerable<GitHubBranch>?>


GetAspNetCoreDocsBranchesAsync() =>
await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
"repos/dotnet/AspNetCore.Docs/branches");
}

In the preceding code:

The configuration is moved into the typed client.


The provided HttpClient instance is stored as a private field.

API-specific methods can be created that expose HttpClient functionality. For example,
the GetAspNetCoreDocsBranches method encapsulates code to retrieve docs GitHub
branches.

The following code calls AddHttpClient in Program.cs to register the GitHubService


typed client class:

C#

builder.Services.AddHttpClient<GitHubService>();

The typed client is registered as transient with DI. In the preceding code, AddHttpClient
registers GitHubService as a transient service. This registration uses a factory method to:

1. Create an instance of HttpClient .


2. Create an instance of GitHubService , passing in the instance of HttpClient to its
constructor.

The typed client can be injected and consumed directly:

C#

public class TypedClientModel : PageModel


{
private readonly GitHubService _gitHubService;

public TypedClientModel(GitHubService gitHubService) =>


_gitHubService = gitHubService;

public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

public async Task OnGet()


{
try
{
GitHubBranches = await
_gitHubService.GetAspNetCoreDocsBranchesAsync();
}
catch (HttpRequestException)
{
// ...
}
}
}

The configuration for a typed client can also be specified during its registration in
Program.cs , rather than in the typed client's constructor:

C#

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");

// ...
});

Generated clients
IHttpClientFactory can be used in combination with third-party libraries such as
Refit . Refit is a REST library for .NET. It converts REST APIs into live interfaces. Call
AddRefitClient to generate a dynamic implementation of an interface, which uses
HttpClient to make the external HTTP calls.

A custom interface represents the external API:

C#

public interface IGitHubClient


{
[Get("/repos/dotnet/AspNetCore.Docs/branches")]
Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}

Call AddRefitClient to generate the dynamic implementation and then call


ConfigureHttpClient to configure the underlying HttpClient :

C#

builder.Services.AddRefitClient<IGitHubClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");

// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});

Use DI to access the dynamic implementation of IGitHubClient :

C#

public class RefitModel : PageModel


{
private readonly IGitHubClient _gitHubClient;

public RefitModel(IGitHubClient gitHubClient) =>


_gitHubClient = gitHubClient;

public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

public async Task OnGet()


{
try
{
GitHubBranches = await
_gitHubClient.GetAspNetCoreDocsBranchesAsync();
}
catch (ApiException)
{
// ...
}
}
}
Make POST, PUT, and DELETE requests
In the preceding examples, all HTTP requests use the GET HTTP verb. HttpClient also
supports other HTTP verbs, including:

POST
PUT
DELETE
PATCH

For a complete list of supported HTTP verbs, see HttpMethod.

The following example shows how to make an HTTP POST request:

C#

public async Task CreateItemAsync(TodoItem todoItem)


{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem),
Encoding.UTF8,
Application.Json); // using static System.Net.Mime.MediaTypeNames;

using var httpResponseMessage =


await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

httpResponseMessage.EnsureSuccessStatusCode();
}

In the preceding code, the CreateItemAsync method:

Serializes the TodoItem parameter to JSON using System.Text.Json .


Creates an instance of StringContent to package the serialized JSON for sending in
the HTTP request's body.
Calls PostAsync to send the JSON content to the specified URL. This is a relative
URL that gets added to the HttpClient.BaseAddress.
Calls EnsureSuccessStatusCode to throw an exception if the response status code
doesn't indicate success.

HttpClient also supports other types of content. For example, MultipartContent and
StreamContent. For a complete list of supported content, see HttpContent.

The following example shows an HTTP PUT request:

C#
public async Task SaveItemAsync(TodoItem todoItem)
{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem),
Encoding.UTF8,
Application.Json);

using var httpResponseMessage =


await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}",
todoItemJson);

httpResponseMessage.EnsureSuccessStatusCode();
}

The preceding code is similar to the POST example. The SaveItemAsync method calls
PutAsync instead of PostAsync .

The following example shows an HTTP DELETE request:

C#

public async Task DeleteItemAsync(long itemId)


{
using var httpResponseMessage =
await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

httpResponseMessage.EnsureSuccessStatusCode();
}

In the preceding code, the DeleteItemAsync method calls DeleteAsync. Because HTTP
DELETE requests typically contain no body, the DeleteAsync method doesn't provide an
overload that accepts an instance of HttpContent .

To learn more about using different HTTP verbs with HttpClient , see HttpClient.

Outgoing request middleware


HttpClient has the concept of delegating handlers that can be linked together for

outgoing HTTP requests. IHttpClientFactory :

Simplifies defining the handlers to apply for each named client.


Supports registration and chaining of multiple handlers to build an outgoing
request middleware pipeline. Each of these handlers is able to perform work before
and after the outgoing request. This pattern:
Is similar to the inbound middleware pipeline in ASP.NET Core.
Provides a mechanism to manage cross-cutting concerns around HTTP requests,
such as:
caching
error handling
serialization
logging

To create a delegating handler:

Derive from DelegatingHandler.


Override SendAsync. Execute code before passing the request to the next handler
in the pipeline:

C#

public class ValidateHeaderHandler : DelegatingHandler


{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!request.Headers.Contains("X-API-KEY"))
{
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent(
"The API key header X-API-KEY is required.")
};
}

return await base.SendAsync(request, cancellationToken);


}
}

The preceding code checks if the X-API-KEY header is in the request. If X-API-KEY is
missing, BadRequest is returned.

More than one handler can be added to the configuration for an HttpClient with
Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessage
Handler:

C#

builder.Services.AddTransient<ValidateHeaderHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
.AddHttpMessageHandler<ValidateHeaderHandler>();
In the preceding code, the ValidateHeaderHandler is registered with DI. Once registered,
AddHttpMessageHandler can be called, passing in the type for the handler.

Multiple handlers can be registered in the order that they should execute. Each handler
wraps the next handler until the final HttpClientHandler executes the request:

C#

builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
.AddHttpMessageHandler<SampleHandler1>()
.AddHttpMessageHandler<SampleHandler2>();

In the preceding code, SampleHandler1 runs first, before SampleHandler2 .

Use DI in outgoing request middleware


When IHttpClientFactory creates a new delegating handler, it uses DI to fulfill the
handler's constructor parameters. IHttpClientFactory creates a separate DI scope for
each handler, which can lead to surprising behavior when a handler consumes a scoped
service.

For example, consider the following interface and its implementation, which represents a
task as an operation with an identifier, OperationId :

C#

public interface IOperationScoped


{
string OperationId { get; }
}

public class OperationScoped : IOperationScoped


{
public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

As its name suggests, IOperationScoped is registered with DI using a scoped lifetime:

C#

builder.Services.AddScoped<IOperationScoped, OperationScoped>();
The following delegating handler consumes and uses IOperationScoped to set the X-
OPERATION-ID header for the outgoing request:

C#

public class OperationHandler : DelegatingHandler


{
private readonly IOperationScoped _operationScoped;

public OperationHandler(IOperationScoped operationScoped) =>


_operationScoped = operationScoped;

protected override async Task<HttpResponseMessage> SendAsync(


HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("X-OPERATION-ID", _operationScoped.OperationId);

return await base.SendAsync(request, cancellationToken);


}
}

In the HttpRequestsSample download , navigate to /Operation and refresh the page.


The request scope value changes for each request, but the handler scope value only
changes every 5 seconds.

Handlers can depend upon services of any scope. Services that handlers depend upon
are disposed when the handler is disposed.

Use one of the following approaches to share per-request state with message handlers:

Pass data into the handler using HttpRequestMessage.Options.


Use IHttpContextAccessor to access the current request.
Create a custom AsyncLocal<T> storage object to pass the data.

Use Polly-based handlers


IHttpClientFactory integrates with the third-party library Polly . Polly is a
comprehensive resilience and transient fault-handling library for .NET. It allows
developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead
Isolation, and Fallback in a fluent and thread-safe manner.

Extension methods are provided to enable the use of Polly policies with configured
HttpClient instances. The Polly extensions support adding Polly-based handlers to
clients. Polly requires the Microsoft.Extensions.Http.Polly NuGet package.
Handle transient faults
Faults typically occur when external HTTP calls are transient. AddTransientHttpErrorPolicy
allows a policy to be defined to handle transient errors. Policies configured with
AddTransientHttpErrorPolicy handle the following responses:

HttpRequestException
HTTP 5xx
HTTP 408

AddTransientHttpErrorPolicy provides access to a PolicyBuilder object configured to

handle errors representing a possible transient fault:

C#

builder.Services.AddHttpClient("PollyWaitAndRetry")
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.WaitAndRetryAsync(
3, retryNumber => TimeSpan.FromMilliseconds(600)));

In the preceding code, a WaitAndRetryAsync policy is defined. Failed requests are retried
up to three times with a delay of 600 ms between attempts.

Dynamically select policies


Extension methods are provided to add Polly-based handlers, for example,
AddPolicyHandler. The following AddPolicyHandler overload inspects the request to
decide which policy to apply:

C#

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(


TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient("PollyDynamic")
.AddPolicyHandler(httpRequestMessage =>
httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy :
longTimeoutPolicy);

In the preceding code, if the outgoing request is an HTTP GET, a 10-second timeout is
applied. For any other HTTP method, a 30-second timeout is used.
Add multiple Polly handlers
It's common to nest Polly policies:

C#

builder.Services.AddHttpClient("PollyMultiple")
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.RetryAsync(3))
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

In the preceding example:

Two handlers are added.


The first handler uses AddTransientHttpErrorPolicy to add a retry policy. Failed
requests are retried up to three times.
The second AddTransientHttpErrorPolicy call adds a circuit breaker policy. Further
external requests are blocked for 30 seconds if 5 failed attempts occur sequentially.
Circuit breaker policies are stateful. All calls through this client share the same
circuit state.

Add policies from the Polly registry


An approach to managing regularly used policies is to define them once and register
them with a PolicyRegistry . For example:

C#

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(


TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));

var policyRegistry = builder.Services.AddPolicyRegistry();

policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);

builder.Services.AddHttpClient("PollyRegistryRegular")
.AddPolicyHandlerFromRegistry("Regular");

builder.Services.AddHttpClient("PollyRegistryLong")
.AddPolicyHandlerFromRegistry("Long");

In the preceding code:


Two policies, Regular and Long , are added to the Polly registry.
AddPolicyHandlerFromRegistry configures individual named clients to use these
policies from the Polly registry.

For more information on IHttpClientFactory and Polly integrations, see the Polly wiki .

HttpClient and lifetime management


A new HttpClient instance is returned each time CreateClient is called on the
IHttpClientFactory . An HttpMessageHandler is created per named client. The factory

manages the lifetimes of the HttpMessageHandler instances.

IHttpClientFactory pools the HttpMessageHandler instances created by the factory to


reduce resource consumption. An HttpMessageHandler instance may be reused from the
pool when creating a new HttpClient instance if its lifetime hasn't expired.

Pooling of handlers is desirable as each handler typically manages its own underlying
HTTP connections. Creating more handlers than necessary can result in connection
delays. Some handlers also keep connections open indefinitely, which can prevent the
handler from reacting to DNS (Domain Name System) changes.

The default handler lifetime is two minutes. The default value can be overridden on a
per named client basis:

C#

builder.Services.AddHttpClient("HandlerLifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));

HttpClient instances can generally be treated as .NET objects not requiring disposal.

Disposal cancels outgoing requests and guarantees the given HttpClient instance can't
be used after calling Dispose. IHttpClientFactory tracks and disposes resources used by
HttpClient instances.

Keeping a single HttpClient instance alive for a long duration is a common pattern
used before the inception of IHttpClientFactory . This pattern becomes unnecessary
after migrating to IHttpClientFactory .

Alternatives to IHttpClientFactory
Using IHttpClientFactory in a DI-enabled app avoids:
Resource exhaustion problems by pooling HttpMessageHandler instances.
Stale DNS problems by cycling HttpMessageHandler instances at regular intervals.

There are alternative ways to solve the preceding problems using a long-lived
SocketsHttpHandler instance.

Create an instance of SocketsHttpHandler when the app starts and use it for the
life of the app.
Configure PooledConnectionLifetime to an appropriate value based on DNS
refresh times.
Create HttpClient instances using new HttpClient(handler, disposeHandler:
false) as needed.

The preceding approaches solve the resource management problems that


IHttpClientFactory solves in a similar way.

The SocketsHttpHandler shares connections across HttpClient instances. This


sharing prevents socket exhaustion.
The SocketsHttpHandler cycles connections according to
PooledConnectionLifetime to avoid stale DNS problems.

Logging
Clients created via IHttpClientFactory record log messages for all requests. Enable the
appropriate information level in the logging configuration to see the default log
messages. Additional logging, such as the logging of request headers, is only included
at trace level.

The log category used for each client includes the name of the client. A client named
MyNamedClient, for example, logs messages with a category of
"System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Messages suffixed with
LogicalHandler occur outside the request handler pipeline. On the request, messages are
logged before any other handlers in the pipeline have processed it. On the response,
messages are logged after any other pipeline handlers have received the response.

Logging also occurs inside the request handler pipeline. In the MyNamedClient example,
those messages are logged with the log category
"System.Net.Http.HttpClient.MyNamedClient.ClientHandler". For the request, this occurs
after all other handlers have run and immediately before the request is sent. On the
response, this logging includes the state of the response before it passes back through
the handler pipeline.
Enabling logging outside and inside the pipeline enables inspection of the changes
made by the other pipeline handlers. This may include changes to request headers or to
the response status code.

Including the name of the client in the log category enables log filtering for specific
named clients.

Configure the HttpMessageHandler


It may be necessary to control the configuration of the inner HttpMessageHandler used
by a client.

An IHttpClientBuilder is returned when adding named or typed clients. The


ConfigurePrimaryHttpMessageHandler extension method can be used to define a
delegate. The delegate is used to create and configure the primary HttpMessageHandler
used by that client:

C#

builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
.ConfigurePrimaryHttpMessageHandler(() =>
new HttpClientHandler
{
AllowAutoRedirect = true,
UseDefaultCredentials = true
});

Cookies
The pooled HttpMessageHandler instances results in CookieContainer objects being
shared. Unanticipated CookieContainer object sharing often results in incorrect code.
For apps that require cookies, consider either:

Disabling automatic cookie handling


Avoiding IHttpClientFactory

Call ConfigurePrimaryHttpMessageHandler to disable automatic cookie handling:

C#

builder.Services.AddHttpClient("NoAutomaticCookies")
.ConfigurePrimaryHttpMessageHandler(() =>
new HttpClientHandler
{
UseCookies = false
});

Use IHttpClientFactory in a console app


In a console app, add the following package references to the project:

Microsoft.Extensions.Hosting
Microsoft.Extensions.Http

In the following example:

IHttpClientFactory and GitHubService are registered in the Generic Host's service


container.
GitHubService is requested from DI, which in-turn requests an instance of

IHttpClientFactory .
GitHubService uses IHttpClientFactory to create an instance of HttpClient , which

it uses to retrieve docs GitHub branches.

C#

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()


.ConfigureServices(services =>
{
services.AddHttpClient();
services.AddTransient<GitHubService>();
})
.Build();

try
{
var gitHubService = host.Services.GetRequiredService<GitHubService>();
var gitHubBranches = await
gitHubService.GetAspNetCoreDocsBranchesAsync();

Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");

if (gitHubBranches is not null)


{
foreach (var gitHubBranch in gitHubBranches)
{
Console.WriteLine($"- {gitHubBranch.Name}");
}
}
}
catch (Exception ex)
{
host.Services.GetRequiredService<ILogger<Program>>()
.LogError(ex, "Unable to load branches from GitHub.");
}

public class GitHubService


{
private readonly IHttpClientFactory _httpClientFactory;

public GitHubService(IHttpClientFactory httpClientFactory) =>


_httpClientFactory = httpClientFactory;

public async Task<IEnumerable<GitHubBranch>?>


GetAspNetCoreDocsBranchesAsync()
{
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
{
Headers =
{
{ "Accept", "application/vnd.github.v3+json" },
{ "User-Agent", "HttpRequestsConsoleSample" }
}
};

var httpClient = _httpClientFactory.CreateClient();


var httpResponseMessage = await
httpClient.SendAsync(httpRequestMessage);

httpResponseMessage.EnsureSuccessStatusCode();

using var contentStream =


await httpResponseMessage.Content.ReadAsStreamAsync();

return await JsonSerializer.DeserializeAsync


<IEnumerable<GitHubBranch>>(contentStream);
}
}

public record GitHubBranch(


[property: JsonPropertyName("name")] string Name);

Header propagation middleware


Header propagation is an ASP.NET Core middleware to propagate HTTP headers from
the incoming request to the outgoing HttpClient requests. To use header propagation:
Install the Microsoft.AspNetCore.HeaderPropagation package.

Configure the HttpClient and middleware pipeline in Program.cs :

C#

// Add services to the container.


builder.Services.AddControllers();

builder.Services.AddHttpClient("PropagateHeaders")
.AddHeaderPropagation();

builder.Services.AddHeaderPropagation(options =>
{
options.Headers.Add("X-TraceId");
});

var app = builder.Build();

// Configure the HTTP request pipeline.


app.UseHttpsRedirection();

app.UseHeaderPropagation();

app.MapControllers();

Make outbound requests using the configured HttpClient instance, which


includes the added headers.

Additional resources
View or download sample code (how to download)
Use HttpClientFactory to implement resilient HTTP requests
Implement HTTP call retries with exponential backoff with HttpClientFactory and
Polly policies
Implement the Circuit Breaker pattern
How to serialize and deserialize JSON in .NET
Static files in ASP.NET Core
Article • 04/06/2023

By Rick Anderson and Kirk Larkin

Static files, such as HTML, CSS, images, and JavaScript, are assets an ASP.NET Core app
serves directly to clients by default.

Serve static files


Static files are stored within the project's web root directory. The default directory is
{content root}/wwwroot , but it can be changed with the UseWebRoot method. For more
information, see Content root and Web root.

The CreateBuilder method sets the content root to the current directory:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

Static files are accessible via a path relative to the web root. For example, the Web
Application project templates contain several folders within the wwwroot folder:

wwwroot

css
js

lib

Consider creating the wwwroot/images folder and adding the


wwwroot/images/MyImage.jpg file. The URI format to access a file in the images folder is
https://<hostname>/images/<image_file_name> . For example,

https://localhost:5001/images/MyImage.jpg

Serve files in web root


The default web app templates call the UseStaticFiles method in Program.cs , which
enables static files to be served:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

The parameterless UseStaticFiles method overload marks the files in web root as
servable. The following markup references wwwroot/images/MyImage.jpg :

HTML

<img src="~/images/MyImage.jpg" class="img" alt="My image" />

In the preceding markup, the tilde character ~ points to the web root.
Serve files outside of web root
Consider a directory hierarchy in which the static files to be served reside outside of the
web root:

wwwroot

css
images

js
MyStaticFiles

images

red-rose.jpg

A request can access the red-rose.jpg file by configuring the Static File Middleware as
follows:

C#

using Microsoft.Extensions.FileProviders;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath,
"MyStaticFiles")),
RequestPath = "/StaticFiles"
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
In the preceding code, the MyStaticFiles directory hierarchy is exposed publicly via the
StaticFiles URI segment. A request to https://<hostname>/StaticFiles/images/red-
rose.jpg serves the red-rose.jpg file.

The following markup references MyStaticFiles/images/red-rose.jpg :

HTML

<img src="~/StaticFiles/images/red-rose.jpg" class="img" alt="A red rose" />

To serve files from multiple locations, see Serve files from multiple locations.

Set HTTP response headers


A StaticFileOptions object can be used to set HTTP response headers. In addition to
configuring static file serving from the web root, the following code sets the Cache-
Control header:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

var cacheMaxAgeOneWeek = (60 * 60 * 24 * 7).ToString();


app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append(
"Cache-Control", $"public, max-age={cacheMaxAgeOneWeek}");
}
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

The preceding code makes static files publicly available in the local cache for one week
(604800 seconds).

Static file authorization


The ASP.NET Core templates call UseStaticFiles before calling UseAuthorization. Most
apps follow this pattern. When the Static File Middleware is called before the
authorization middleware:

No authorization checks are performed on the static files.


Static files served by the Static File Middleware, such as those under wwwroot , are
publicly accessible.

To serve static files based on authorization:

Store them outside of wwwroot .


Call UseStaticFiles , specifying a path, after calling UseAuthorization .
Set the fallback authorization policy.

C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using StaticFileAuth.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

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

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath,
"MyStaticFiles")),
RequestPath = "/StaticFiles"
});

app.MapRazorPages();

app.Run();

In the preceding code, the fallback authorization policy requires all users to be
authenticated. Endpoints such as controllers, Razor Pages, etc that specify their own
authorization requirements don't use the fallback authorization policy. For example,
Razor Pages, controllers, or action methods with [AllowAnonymous] or
[Authorize(PolicyName="MyPolicy")] use the applied authorization attribute rather than
the fallback authorization policy.

RequireAuthenticatedUser adds DenyAnonymousAuthorizationRequirement to the


current instance, which enforces that the current user is authenticated.
Static assets under wwwroot are publicly accessible because the default Static File
Middleware ( app.UseStaticFiles(); ) is called before UseAuthentication . Static assets in
the MyStaticFiles folder require authentication. The sample code demonstrates this.

An alternative approach to serve files based on authorization is to:

Store them outside of wwwroot and any directory accessible to the Static File
Middleware.
Serve them via an action method to which authorization is applied and return a
FileResult object:

C#

[Authorize]
public class BannerImageModel : PageModel
{
private readonly IWebHostEnvironment _env;

public BannerImageModel(IWebHostEnvironment env) =>


_env = env;

public PhysicalFileResult OnGet()


{
var filePath = Path.Combine(
_env.ContentRootPath, "MyStaticFiles", "images", "red-
rose.jpg");

return PhysicalFile(filePath, "image/jpeg");


}
}

Directory browsing
Directory browsing allows directory listing within specified directories.

Directory browsing is disabled by default for security reasons. For more information, see
Security considerations for static files.

Enable directory browsing with AddDirectoryBrowser and UseDirectoryBrowser:

C#

using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDirectoryBrowser();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

var fileProvider = new


PhysicalFileProvider(Path.Combine(builder.Environment.WebRootPath,
"images"));
var requestPath = "/MyImages";

// Enable displaying browser links.


app.UseStaticFiles(new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = requestPath
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = fileProvider,
RequestPath = requestPath
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

The preceding code allows directory browsing of the wwwroot/images folder using the
URL https://<hostname>/MyImages , with links to each file and folder:
AddDirectoryBrowser adds services required by the directory browsing middleware,
including HtmlEncoder. These services may be added by other calls, such as
AddRazorPages, but we recommend calling AddDirectoryBrowser to ensure the services
are added in all apps.

Serve default documents


Setting a default page provides visitors a starting point on a site. To serve a default file
from wwwroot without requiring the request URL to include the file's name, call the
UseDefaultFiles method:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseDefaultFiles();

app.UseStaticFiles();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
UseDefaultFiles must be called before UseStaticFiles to serve the default file.

UseDefaultFiles is a URL rewriter that doesn't serve the file.

With UseDefaultFiles , requests to a folder in wwwroot search for:

default.htm
default.html

index.htm

index.html

The first file found from the list is served as though the request included the file's name.
The browser URL continues to reflect the URI requested.

The following code changes the default file name to mydefault.html :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

var options = new DefaultFilesOptions();


options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(options);

app.UseStaticFiles();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

UseFileServer for default documents


UseFileServer combines the functionality of UseStaticFiles , UseDefaultFiles , and
optionally UseDirectoryBrowser .

Call app.UseFileServer to enable the serving of static files and the default file. Directory
browsing isn't enabled:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseFileServer();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

The following code enables the serving of static files, the default file, and directory
browsing:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDirectoryBrowser();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();

app.UseFileServer(enableDirectoryBrowsing: true);

app.UseRouting();

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

Consider the following directory hierarchy:

wwwroot

css

images
js

MyStaticFiles
images

MyImage.jpg

default.html

The following code enables the serving of static files, the default file, and directory
browsing of MyStaticFiles :

C#

using Microsoft.Extensions.FileProviders;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDirectoryBrowser();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath,
"MyStaticFiles")),
RequestPath = "/StaticFiles",
EnableDirectoryBrowsing = true
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

AddDirectoryBrowser must be called when the EnableDirectoryBrowsing property value


is true .

Using the preceding file hierarchy and code, URLs resolve as follows:

URI Response

https://<hostname>/StaticFiles/images/MyImage.jpg MyStaticFiles/images/MyImage.jpg

https://<hostname>/StaticFiles MyStaticFiles/default.html

If no default-named file exists in the MyStaticFiles directory,


https://<hostname>/StaticFiles returns the directory listing with clickable links:

UseDefaultFiles and UseDirectoryBrowser perform a client-side redirect from the target


URI without a trailing / to the target URI with a trailing / . For example, from
https://<hostname>/StaticFiles to https://<hostname>/StaticFiles/ . Relative URLs

within the StaticFiles directory are invalid without a trailing slash ( / ) unless the
RedirectToAppendTrailingSlash option of DefaultFilesOptions is used.

FileExtensionContentTypeProvider
The FileExtensionContentTypeProvider class contains a Mappings property that serves as
a mapping of file extensions to MIME content types. In the following sample, several file
extensions are mapped to known MIME types. The .rtf extension is replaced, and .mp4 is
removed:

C#

using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

// Set up custom content types - associating file extension to MIME type


var provider = new FileExtensionContentTypeProvider();
// Add new mappings
provider.Mappings[".myapp"] = "application/x-msdownload";
provider.Mappings[".htm3"] = "text/html";
provider.Mappings[".image"] = "image/png";
// Replace an existing mapping
provider.Mappings[".rtf"] = "application/x-msdownload";
// Remove MP4 videos.
provider.Mappings.Remove(".mp4");

app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();

See MIME content types .

Non-standard content types


The Static File Middleware understands almost 400 known file content types. If the user
requests a file with an unknown file type, the Static File Middleware passes the request
to the next middleware in the pipeline. If no middleware handles the request, a 404 Not
Found response is returned. If directory browsing is enabled, a link to the file is
displayed in a directory listing.

The following code enables serving unknown types and renders the unknown file as an
image:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true,
DefaultContentType = "image/png"
});

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
With the preceding code, a request for a file with an unknown content type is returned
as an image.

2 Warning

Enabling ServeUnknownFileTypes is a security risk. It's disabled by default, and its


use is discouraged. FileExtensionContentTypeProvider provides a safer alternative
to serving files with non-standard extensions.

Serve files from multiple locations


Consider the following Razor page which displays the /MyStaticFiles/image3.png file:

CSHTML

@page

<p> Test /MyStaticFiles/image3.png</p>

<img src="~/image3.png" class="img" asp-append-version="true" alt="Test">

UseStaticFiles and UseFileServer default to the file provider pointing at wwwroot .


Additional instances of UseStaticFiles and UseFileServer can be provided with other
file providers to serve files from other locations. The following example calls
UseStaticFiles twice to serve files from both wwwroot and MyStaticFiles :

C#

app.UseStaticFiles(); // Serve files from wwwroot


app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles"))
});

Using the preceding code:

The /MyStaticFiles/image3.png file is displayed.


The Image Tag Helpers AppendVersion is not applied because the Tag Helpers
depend on WebRootFileProvider. WebRootFileProvider has not been updated to
include the MyStaticFiles folder.
The following code updates the WebRootFileProvider , which enables the Image Tag
Helper to provide a version:

C#

var webRootProvider = new


PhysicalFileProvider(builder.Environment.WebRootPath);
var newPathProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles"));

var compositeProvider = new CompositeFileProvider(webRootProvider,


newPathProvider);

// Update the default provider.


app.Environment.WebRootFileProvider = compositeProvider;

app.UseStaticFiles();

Security considerations for static files

2 Warning

UseDirectoryBrowser and UseStaticFiles can leak secrets. Disabling directory

browsing in production is highly recommended. Carefully review which directories


are enabled via UseStaticFiles or UseDirectoryBrowser . The entire directory and
its sub-directories become publicly accessible. Store files suitable for serving to the
public in a dedicated directory, such as <content_root>/wwwroot . Separate these
files from MVC views, Razor Pages, configuration files, etc.

The URLs for content exposed with UseDirectoryBrowser and UseStaticFiles are
subject to the case sensitivity and character restrictions of the underlying file
system. For example, Windows is case insensitive, but macOS and Linux aren't.

ASP.NET Core apps hosted in IIS use the ASP.NET Core Module to forward all
requests to the app, including static file requests. The IIS static file handler isn't
used and has no chance to handle requests.

Complete the following steps in IIS Manager to remove the IIS static file handler at
the server or website level:

1. Navigate to the Modules feature.


2. Select StaticFileModule in the list.
3. Click Remove in the Actions sidebar.
2 Warning

If the IIS static file handler is enabled and the ASP.NET Core Module is configured
incorrectly, static files are served. This happens, for example, if the web.config file
isn't deployed.

Place code files, including .cs and .cshtml , outside of the app project's web root.
A logical separation is therefore created between the app's client-side content and
server-based code. This prevents server-side code from being leaked.

Serve files outside wwwroot by updating


IWebHostEnvironment.WebRootPath
When IWebHostEnvironment.WebRootPath is set to a folder other than wwwroot :

In the development environment, static assets found in both wwwroot and the
updated IWebHostEnvironment.WebRootPath are served from wwwroot .
In any environment other than development, duplicate static assets are served
from the updated IWebHostEnvironment.WebRootPath folder.

Consider a web app created with the empty web template:

Containing an Index.html file in wwwroot and wwwroot-custom .

With the following updated Program.cs file that sets WebRootPath = "wwwroot-
custom" :

C#

var builder = WebApplication.CreateBuilder(new WebApplicationOptions


{
Args = args,
// Look for static files in "wwwroot-custom"
WebRootPath = "wwwroot-custom"
});

var app = builder.Build();

app.UseDefaultFiles();
app.UseStaticFiles();

app.Run();
In the preceding code, requests to / :

In the development environment return wwwroot/Index.html


In any environment other than development return wwwroot-custom/Index.html

To ensure assets from wwwroot-custom are returned, use one of the following
approaches:

Delete duplicate named assets in wwwroot .

Set "ASPNETCORE_ENVIRONMENT" in Properties/launchSettings.json to any value


other than "Development" .

Completely disable static web assets by setting


<StaticWebAssetsEnabled>false</StaticWebAssetsEnabled> in the project file.

WARNING, disabling static web assets disables Razor Class Libraries.

Add the following JSON to the project file:

XML

<ItemGroup>
<Content Remove="wwwroot\**" />
</ItemGroup>

The following code updates IWebHostEnvironment.WebRootPath to a non development


value, guaranteeing duplicate content is returned from wwwroot-custom rather than
wwwroot :

C#

var builder = WebApplication.CreateBuilder(new WebApplicationOptions


{
Args = args,
// Examine Hosting environment: logging value
EnvironmentName = Environments.Staging,
WebRootPath = "wwwroot-custom"
});

var app = builder.Build();

app.Logger.LogInformation("ASPNETCORE_ENVIRONMENT: {env}",
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"));

app.Logger.LogInformation("app.Environment.IsDevelopment(): {env}",
app.Environment.IsDevelopment().ToString());

app.UseDefaultFiles();
app.UseStaticFiles();

app.Run();

Additional resources
View or download sample code (how to download)
Middleware
Introduction to ASP.NET Core
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
Introduction to Razor Pages in ASP.NET
Core
Article • 03/25/2023

By Rick Anderson , Dave Brock , and Kirk Larkin

Razor Pages can make coding page-focused scenarios easier and more productive than
using controllers and views.

If you're looking for a tutorial that uses the Model-View-Controller approach, see Get
started with ASP.NET Core MVC.

This document provides an introduction to Razor Pages. It's not a step by step tutorial. If
you find some of the sections too advanced, see Get started with Razor Pages. For an
overview of ASP.NET Core, see the Introduction to ASP.NET Core.

Prerequisites
Visual Studio

Visual Studio 2022 with the ASP.NET and web development workload.

Create a Razor Pages project


Visual Studio

See Get started with Razor Pages for detailed instructions on how to create a Razor
Pages project.

Razor Pages
Razor Pages is enabled in Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

In the preceding code:

AddRazorPages adds services for Razor Pages to the app.


MapRazorPages adds endpoints for Razor Pages to the IEndpointRouteBuilder.

Consider a basic page:

CSHTML

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

The preceding code looks a lot like a Razor view file used in an ASP.NET Core app with
controllers and views. What makes it different is the @page directive. @page makes the
file into an MVC action, which means that it handles requests directly, without going
through a controller. @page must be the first Razor directive on a page. @page affects
the behavior of other Razor constructs. Razor Pages file names have a .cshtml suffix.

A similar page, using a PageModel class, is shown in the following two files. The
Pages/Index2.cshtml file:

CSHTML

@page
@using RazorPagesIntro.Pages
@model Index2Model
<h2>Separate page model</h2>
<p>
@Model.Message
</p>

The Pages/Index2.cshtml.cs page model:

C#

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
public class Index2Model : PageModel
{
public string Message { get; private set; } = "PageModel in C#";

public void OnGet()


{
Message += $" Server time is { DateTime.Now }";
}
}
}

By convention, the PageModel class file has the same name as the Razor Page file with
.cs appended. For example, the previous Razor Page is Pages/Index2.cshtml . The file
containing the PageModel class is named Pages/Index2.cshtml.cs .

The associations of URL paths to pages are determined by the page's location in the file
system. The following table shows a Razor Page path and the matching URL:

File name and path matching URL

/Pages/Index.cshtml / or /Index

/Pages/Contact.cshtml /Contact

/Pages/Store/Contact.cshtml /Store/Contact

/Pages/Store/Index.cshtml /Store or /Store/Index

Notes:

The runtime looks for Razor Pages files in the Pages folder by default.
Index is the default page when a URL doesn't include a page.
Write a basic form
Razor Pages is designed to make common patterns used with web browsers easy to
implement when building an app. Model binding, Tag Helpers, and HTML helpers work
with the properties defined in a Razor Page class. Consider a page that implements a
basic "contact us" form for the Contact model:

For the samples in this document, the DbContext is initialized in the Startup.cs file.

The in memory database requires the Microsoft.EntityFrameworkCore.InMemory NuGet


package.

C#

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<CustomerDbContext>(options =>
options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

The data model:

C#

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
public class Customer
{
public int Id { get; set; }

[Required, StringLength(10)]
public string? Name { get; set; }
}
}

The db context:

C#

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
public class CustomerDbContext : DbContext
{
public CustomerDbContext (DbContextOptions<CustomerDbContext>
options)
: base(options)
{
}

public DbSet<RazorPagesContacts.Models.Customer> Customer =>


Set<RazorPagesContacts.Models.Customer>();
}
}

The Pages/Customers/Create.cshtml view file:

CSHTML

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
Name:
<input asp-for="Customer!.Name" />
<input type="submit" />
</form>

The Pages/Customers/Create.cshtml.cs page model:

C#
public class CreateModel : PageModel
{
private readonly Data.CustomerDbContext _context;

public CreateModel(Data.CustomerDbContext context)


{
_context = context;
}

public IActionResult OnGet()


{
return Page();
}

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

if (Customer != null) _context.Customer.Add(Customer);


await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}

By convention, the PageModel class is called <PageName>Model and is in the same


namespace as the page.

The PageModel class allows separation of the logic of a page from its presentation. It
defines page handlers for requests sent to the page and the data used to render the
page. This separation allows:

Managing of page dependencies through dependency injection.


Unit testing

The page has an OnPostAsync handler method, which runs on POST requests (when a
user posts the form). Handler methods for any HTTP verb can be added. The most
common handlers are:

OnGet to initialize state needed for the page. In the preceding code, the OnGet

method displays the CreateModel.cshtml Razor Page.


OnPost to handle form submissions.
The Async naming suffix is optional but is often used by convention for asynchronous
functions. The preceding code is typical for Razor Pages.

If you're familiar with ASP.NET apps using controllers and views:

The OnPostAsync code in the preceding example looks similar to typical controller
code.
Most of the MVC primitives like model binding, validation, and action results work
the same with Controllers and Razor Pages.

The previous OnPostAsync method:

C#

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

if (Customer != null) _context.Customer.Add(Customer);


await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

The basic flow of OnPostAsync :

Check for validation errors.

If there are no errors, save the data and redirect.


If there are errors, show the page again with validation messages. In many cases,
validation errors would be detected on the client, and never submitted to the
server.

The Pages/Customers/Create.cshtml view file:

CSHTML

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>


<form method="post">
Name:
<input asp-for="Customer!.Name" />
<input type="submit" />
</form>

The rendered HTML from Pages/Customers/Create.cshtml :

HTML

<p>Enter a customer name:</p>

<form method="post">
Name:
<input type="text" data-val="true"
data-val-length="The field Name must be a string with a maximum
length of 10."
data-val-length-max="10" data-val-required="The Name field is
required."
id="Customer_Name" maxlength="10" name="Customer.Name" value=""
/>
<input type="submit" />
<input name="__RequestVerificationToken" type="hidden"
value="<Antiforgery token here>" />
</form>

In the previous code, posting the form:

With valid data:

The OnPostAsync handler method calls the RedirectToPage helper method.


RedirectToPage returns an instance of RedirectToPageResult. RedirectToPage :

Is an action result.
Is similar to RedirectToAction or RedirectToRoute (used in controllers and
views).
Is customized for pages. In the preceding sample, it redirects to the root
Index page ( /Index ). RedirectToPage is detailed in the URL generation for
Pages section.

With validation errors that are passed to the server:


The OnPostAsync handler method calls the Page helper method. Page returns an
instance of PageResult. Returning Page is similar to how actions in controllers
return View . PageResult is the default return type for a handler method. A
handler method that returns void renders the page.
In the preceding example, posting the form with no value results in
ModelState.IsValid returning false. In this sample, no validation errors are
displayed on the client. Validation error handling is covered later in this
document.

C#

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

if (Customer != null) _context.Customer.Add(Customer);


await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}

With validation errors detected by client side validation:


Data is not posted to the server.
Client-side validation is explained later in this document.

The Customer property uses [BindProperty] attribute to opt in to model binding:

C#

public class CreateModel : PageModel


{
private readonly Data.CustomerDbContext _context;

public CreateModel(Data.CustomerDbContext context)


{
_context = context;
}

public IActionResult OnGet()


{
return Page();
}

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

if (Customer != null) _context.Customer.Add(Customer);


await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}

[BindProperty] should not be used on models containing properties that should not be
changed by the client. For more information, see Overposting.

Razor Pages, by default, bind properties only with non- GET verbs. Binding to properties
removes the need to writing code to convert HTTP data to the model type. Binding
reduces code by using the same property to render form fields ( <input asp-
for="Customer.Name"> ) and accept the input.

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) .

Reviewing the Pages/Customers/Create.cshtml view file:

CSHTML

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<p>Enter a customer name:</p>

<form method="post">
Name:
<input asp-for="Customer!.Name" />
<input type="submit" />
</form>

In the preceding code, the input tag helper <input asp-for="Customer.Name" />
binds the HTML <input> element to the Customer.Name model expression.
@addTagHelper makes Tag Helpers available.

The home page


Index.cshtml is the home page:

CSHTML

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>


<form method="post">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
@if (Model.Customers != null)
{
foreach (var contact in Model.Customers)
{
<tr>
<td> @contact.Id </td>
<td>@contact.Name</td>
<td>
<!-- <snippet_Edit> -->
<a asp-page="./Edit" asp-route-
id="@contact.Id">Edit</a> |
<!-- </snippet_Edit> -->
<!-- <snippet_Delete> -->
<button type="submit" asp-page-handler="delete" asp-
route-id="@contact.Id">delete</button>
<!-- </snippet_Delete> -->
</td>
</tr>
}
}
</tbody>
</table>
<a asp-page="Create">Create New</a>
</form>

The associated PageModel class ( Index.cshtml.cs ):

C#

public class IndexModel : PageModel


{
private readonly Data.CustomerDbContext _context;
public IndexModel(Data.CustomerDbContext context)
{
_context = context;
}

public IList<Customer>? Customers { get; set; }

public async Task OnGetAsync()


{
Customers = await _context.Customer.ToListAsync();
}

public async Task<IActionResult> OnPostDeleteAsync(int id)


{
var contact = await _context.Customer.FindAsync(id);

if (contact != null)
{
_context.Customer.Remove(contact);
await _context.SaveChangesAsync();
}

return RedirectToPage();
}
}

The Index.cshtml file contains the following markup:

CSHTML

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |

The <a /a> Anchor Tag Helper used the asp-route-{value} attribute to generate a link
to the Edit page. The link contains route data with the contact ID. For example,
https://localhost:5001/Edit/1 . Tag Helpers enable server-side code to participate in

creating and rendering HTML elements in Razor files.

The Index.cshtml file contains markup to create a delete button for each customer
contact:

CSHTML

<button type="submit" asp-page-handler="delete" asp-route-


id="@contact.Id">delete</button>

The rendered HTML:

HTML

<button type="submit" formaction="/Customers?


id=1&amp;handler=delete">delete</button>

When the delete button is rendered in HTML, its formaction includes parameters for:

The customer contact ID, specified by the asp-route-id attribute.


The handler , specified by the asp-page-handler attribute.

When the button is selected, a form POST request is sent to the server. By convention,
the name of the handler method is selected based on the value of the handler
parameter according to the scheme OnPost[handler]Async .

Because the handler is delete in this example, the OnPostDeleteAsync handler method
is used to process the POST request. If the asp-page-handler is set to a different value,
such as remove , a handler method with the name OnPostRemoveAsync is selected.

C#

public async Task<IActionResult> OnPostDeleteAsync(int id)


{
var contact = await _context.Customer.FindAsync(id);

if (contact != null)
{
_context.Customer.Remove(contact);
await _context.SaveChangesAsync();
}

return RedirectToPage();
}
The OnPostDeleteAsync method:

Gets the id from the query string.


Queries the database for the customer contact with FindAsync .
If the customer contact is found, it's removed and the database is updated.
Calls RedirectToPage to redirect to the root Index page ( /Index ).

The Edit.cshtml file


CSHTML

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel

@{
ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Customer</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="Customer!.Id" />
<div class="form-group">
<label asp-for="Customer!.Name" class="control-label">
</label>
<input asp-for="Customer!.Name" class="form-control" />
<span asp-validation-for="Customer!.Name" 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 first line contains the @page "{id:int}" directive. The routing constraint "{id:int}"
tells the page to accept requests to the page that contain int route data. If a request to
the page doesn't contain route data that can be converted to an int , the runtime
returns an HTTP 404 (not found) error. To make the ID optional, append ? to the route
constraint:

CSHTML

@page "{id:int?}"

The Edit.cshtml.cs file:

C#

public class EditModel : PageModel


{
private readonly RazorPagesContacts.Data.CustomerDbContext _context;

public EditModel(RazorPagesContacts.Data.CustomerDbContext context)


{
_context = context;
}

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnGetAsync(int? id)


{
if (id == null)
{
return NotFound();
}

Customer = await _context.Customer.FirstOrDefaultAsync(m => m.Id ==


id);

if (Customer == null)
{
return NotFound();
}
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();
}

if (Customer != null)
{
_context.Attach(Customer).State = EntityState.Modified;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CustomerExists(Customer.Id))
{
return NotFound();
}
else
{
throw;
}
}
}

return RedirectToPage("./Index");
}

private bool CustomerExists(int id)


{
return _context.Customer.Any(e => e.Id == id);
}
}

Validation
Validation rules:

Are declaratively specified in the model class.


Are enforced everywhere in the app.

The System.ComponentModel.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.

Consider the Customer model:

C#
using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
public class Customer
{
public int Id { get; set; }

[Required, StringLength(10)]
public string? Name { get; set; }
}
}

Using the following Create.cshtml view file:

CSHTML

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<span asp-validation-for="Customer.Name"></span>
Name:
<input asp-for="Customer.Name" />
<input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-
unobtrusive/jquery.validate.unobtrusive.js"></script>

The preceding code:

Includes jQuery and jQuery validation scripts.

Uses the <div /> and <span /> Tag Helpers to enable:
Client-side validation.
Validation error rendering.

Generates the following HTML:

HTML
<p>Enter a customer name:</p>

<form method="post">
Name:
<input type="text" data-val="true"
data-val-length="The field Name must be a string with a
maximum length of 10."
data-val-length-max="10" data-val-required="The Name field
is required."
id="Customer_Name" maxlength="10" name="Customer.Name"
value="" />
<input type="submit" />
<input name="__RequestVerificationToken" type="hidden"
value="<Antiforgery token here>" />
</form>

<script src="/lib/jquery/dist/jquery.js"></script>
<script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="/lib/jquery-validation-
unobtrusive/jquery.validate.unobtrusive.js"></script>

Posting the Create form without a name value displays the error message "The Name
field is required." on the form. If JavaScript is enabled on the client, the browser displays
the error without posting to the server.

The [StringLength(10)] attribute generates data-val-length-max="10" on the rendered


HTML. data-val-length-max prevents browsers from entering more than the maximum
length specified. If a tool such as Fiddler is used to edit and replay the post:

With the name longer than 10.


The error message "The field Name must be a string with a maximum length of
10." is returned.

Consider the following Movie model:

C#

using System;
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; }

[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 to enforce on the model properties they're
applied to:

The Required and MinimumLength attributes indicate that a property must have a
value, but nothing prevents a user from entering white space to satisfy this
validation.

The RegularExpression attribute is used to limit what characters can be input. In


the preceding code, "Genre":
Must only use letters.
The first letter is required to be uppercase. White space, numbers, and special
characters are not allowed.

The RegularExpression "Rating":


Requires that the first character be an uppercase letter.
Allows special characters and numbers in subsequent spaces. "PG-13" is valid
for a rating, but fails for a "Genre".

The Range attribute constrains a value to within a specified range.

The StringLength attribute sets 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.
The Create page for the Movie model shows displays errors with invalid values:

For more information, see:

Add validation to the Movie app


Model validation in ASP.NET Core.
CSS isolation
Isolate CSS styles to individual pages, views, and components to reduce or avoid:

Dependencies on global styles that can be challenging to maintain.


Style conflicts in nested content.

To add a scoped CSS file for a page or view, place the CSS styles in a companion
.cshtml.css file matching the name of the .cshtml file. In the following example, an
Index.cshtml.css file supplies CSS styles that are only applied to the Index.cshtml page

or view.

Pages/Index.cshtml.css (Razor Pages) or Views/Index.cshtml.css (MVC):

css

h1 {
color: red;
}

CSS isolation occurs at build time. The framework rewrites CSS selectors to match
markup rendered by the app's pages or views. The rewritten CSS styles are bundled and
produced as a static asset, {APP ASSEMBLY}.styles.css . The placeholder {APP ASSEMBLY}
is the assembly name of the project. A link to the bundled CSS styles is placed in the
app's layout.

In the <head> content of the app's Pages/Shared/_Layout.cshtml (Razor Pages) or


Views/Shared/_Layout.cshtml (MVC), add or confirm the presence of the link to the
bundled CSS styles:

HTML

<link rel="stylesheet" href="~/{APP ASSEMBLY}.styles.css" />

In the following example, the app's assembly name is WebApp :

HTML

<link rel="stylesheet" href="WebApp.styles.css" />

The styles defined in a scoped CSS file are only applied to the rendered output of the
matching file. In the preceding example, any h1 CSS declarations defined elsewhere in
the app don't conflict with the Index 's heading style. CSS style cascading and
inheritance rules remain in effect for scoped CSS files. For example, styles applied
directly to an <h1> element in the Index.cshtml file override the scoped CSS file's styles
in Index.cshtml.css .

7 Note

In order to guarantee CSS style isolation when bundling occurs, importing CSS in
Razor code blocks isn't supported.

CSS isolation only applies to HTML elements. CSS isolation isn't supported for Tag
Helpers.

Within the bundled CSS file, each page, view, or Razor component is associated with a
scope identifier in the format b-{STRING} , where the {STRING} placeholder is a ten-
character string generated by the framework. The following example provides the style
for the preceding <h1> element in the Index page of a Razor Pages app:

css

/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
color: red;
}

In the Index page where the CSS style is applied from the bundled file, the scope
identifier is appended as an HTML attribute:

HTML

<h1 b-3xxtam6d07>

The identifier is unique to an app. At build time, a project bundle is created with the
convention {STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css , where the placeholder
{STATIC WEB ASSETS BASE PATH} is the static web assets base path.

If other projects are utilized, such as NuGet packages or Razor class libraries, the
bundled file:

References the styles using CSS imports.


Isn't published as a static web asset of the app that consumes the styles.

CSS preprocessor support


CSS preprocessors are useful for improving CSS development by utilizing features such
as variables, nesting, modules, mixins, and inheritance. While CSS isolation doesn't
natively support CSS preprocessors such as Sass or Less, integrating CSS preprocessors
is seamless as long as preprocessor compilation occurs before the framework rewrites
the CSS selectors during the build process. Using Visual Studio for example, configure
existing preprocessor compilation as a Before Build task in the Visual Studio Task
Runner Explorer.

Many third-party NuGet packages, such as AspNetCore.SassCompiler , can compile


SASS/SCSS files at the beginning of the build process before CSS isolation occurs, and
no additional configuration is required.

CSS isolation configuration


CSS isolation permits configuration for some advanced scenarios, such as when there
are dependencies on existing tools or workflows.

Customize scope identifier format


In this section, the {Pages|Views} placeholder is either Pages for Razor Pages apps or
Views for MVC apps.

By default, scope identifiers use the format b-{STRING} , where the {STRING} placeholder
is a ten-character string generated by the framework. To customize the scope identifier
format, update the project file to a desired pattern:

XML

<ItemGroup>
<None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-
identifier" />
</ItemGroup>

In the preceding example, the CSS generated for Index.cshtml.css changes its scope
identifier from b-{STRING} to custom-scope-identifier .

Use scope identifiers to achieve inheritance with scoped CSS files. In the following
project file example, a BaseView.cshtml.css file contains common styles across views. A
DerivedView.cshtml.css file inherits these styles.

XML
<ItemGroup>
<None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-
identifier" />
<None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-
scope-identifier" />
</ItemGroup>

Use the wildcard ( * ) operator to share scope identifiers across multiple files:

XML

<ItemGroup>
<None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-
identifier" />
</ItemGroup>

Change base path for static web assets


The scoped CSS file is generated at the root of the app. In the project file, use the
StaticWebAssetBasePath property to change the default path. The following example

places the scoped CSS file, and the rest of the app's assets, at the _content path:

XML

<PropertyGroup>
<StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>

Disable automatic bundling


To opt out of how framework publishes and loads scoped files at runtime, use the
DisableScopedCssBundling property. When using this property, other tools or processes
are responsible for taking the isolated CSS files from the obj directory and publishing
and loading them at runtime:

XML

<PropertyGroup>
<DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>

Razor class library (RCL) support


When a Razor class library (RCL) provides isolated styles, the <link> tag's href attribute
points to {STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css , where the
placeholders are:

{STATIC WEB ASSET BASE PATH} : The static web asset base path.
{PACKAGE ID} : The library's package identifier. The package identifier defaults to

the project's assembly name if the package identifier isn't specified in the project
file.

In the following example:

The static web asset base path is _content/ClassLib .


The class library's assembly name is ClassLib .

Pages/Shared/_Layout.cshtml (Razor Pages) or Views/Shared/_Layout.cshtml (MVC):

HTML

<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">

For more information on RCLs, see the following articles:

Reusable Razor UI in class libraries with ASP.NET Core


Consume ASP.NET Core Razor components from a Razor class library (RCL)

For information on Blazor CSS isolation, see ASP.NET Core Blazor CSS isolation.

Handle HEAD requests with an OnGet handler


fallback
HEAD requests allow retrieving the headers for a specific resource. Unlike GET requests,
HEAD requests don't return a response body.

Ordinarily, an OnHead handler is created and called for HEAD requests:

C#

public void OnHead()


{
HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

Razor Pages falls back to calling the OnGet handler if no OnHead handler is defined.
XSRF/CSRF and Razor Pages
Razor Pages are protected by Antiforgery validation. The FormTagHelper injects
antiforgery tokens into HTML form elements.

Using Layouts, partials, templates, and Tag


Helpers with Razor Pages
Pages work with all the capabilities of the Razor view engine. Layouts, partials,
templates, Tag Helpers, _ViewStart.cshtml , and _ViewImports.cshtml work in the same
way they do for conventional Razor views.

Let's declutter this page by taking advantage of some of those capabilities.

Add a layout page to Pages/Shared/_Layout.cshtml :

CSHTML

<!DOCTYPE html>
<html>
<head>
<title>RP Sample</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
<a asp-page="/Index">Home</a>
<a asp-page="/Customers/Create">Create</a>
<a asp-page="/Customers/Index">Customers</a> <br />

@RenderBody()
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-
unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

The Layout:

Controls the layout of each page (unless the page opts out of layout).
Imports HTML structures such as JavaScript and stylesheets.
The contents of the Razor page are rendered where @RenderBody() is called.

For more information, see layout page.

The Layout property is set in Pages/_ViewStart.cshtml :


CSHTML

@{
Layout = "_Layout";
}

The layout is in the Pages/Shared folder. Pages look for other views (layouts, templates,
partials) hierarchically, starting in the same folder as the current page. A layout in the
Pages/Shared folder can be used from any Razor page under the Pages folder.

The layout file should go in the Pages/Shared folder.

We recommend you not put the layout file in the Views/Shared folder. Views/Shared is
an MVC views pattern. Razor Pages are meant to rely on folder hierarchy, not path
conventions.

View search from a Razor Page includes the Pages folder. The layouts, templates, and
partials used with MVC controllers and conventional Razor views just work.

Add a Pages/_ViewImports.cshtml file:

CSHTML

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace is explained later in the tutorial. The @addTagHelper directive brings in the
built-in Tag Helpers to all the pages in the Pages folder.

The @namespace directive set on a page:

CSHTML

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
@Model.Message
</p>

The @namespace directive sets the namespace for the page. The @model directive doesn't
need to include the namespace.
When the @namespace directive is contained in _ViewImports.cshtml , the specified
namespace supplies the prefix for the generated namespace in the Page that imports
the @namespace directive. The rest of the generated namespace (the suffix portion) is the
dot-separated relative path between the folder containing _ViewImports.cshtml and the
folder containing the page.

For example, the PageModel class Pages/Customers/Edit.cshtml.cs explicitly sets the


namespace:

C#

namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;

public EditModel(AppDbContext db)


{
_db = db;
}

// Code removed for brevity.

The Pages/_ViewImports.cshtml file sets the following namespace:

CSHTML

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The generated namespace for the Pages/Customers/Edit.cshtml Razor Page is the same
as the PageModel class.

@namespace also works with conventional Razor views.

Consider the Pages/Customers/Create.cshtml view file:

CSHTML

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<span asp-validation-for="Customer.Name"></span>
Name:
<input asp-for="Customer.Name" />
<input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-
unobtrusive/jquery.validate.unobtrusive.js"></script>

The updated Pages/Customers/Create.cshtml view file with _ViewImports.cshtml and the


preceding layout file:

CSHTML

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
Name:
<input asp-for="Customer.Name" />
<input type="submit" />
</form>

In the preceding code, the _ViewImports.cshtml imported the namespace and Tag
Helpers. The layout file imported the JavaScript files.

The Razor Pages starter project contains the Pages/_ValidationScriptsPartial.cshtml ,


which hooks up client-side validation.

For more information on partial views, see Partial views in ASP.NET Core.

URL generation for Pages


The Create page, shown previously, uses RedirectToPage :

C#

public class CreateModel : PageModel


{
private readonly CustomerDbContext _context;

public CreateModel(CustomerDbContext context)


{
_context = context;
}

public IActionResult OnGet()


{
return Page();
}

[BindProperty]
public Customer Customer { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Customers.Add(Customer);
await _context.SaveChangesAsync();

return RedirectToPage("./Index");
}
}

The app has the following file/folder structure:

/Pages

Index.cshtml

Privacy.cshtml

/Customers
Create.cshtml
Edit.cshtml

Index.cshtml

The Pages/Customers/Create.cshtml and Pages/Customers/Edit.cshtml pages redirect to


Pages/Customers/Index.cshtml after success. The string ./Index is a relative page name

used to access the preceding page. It is used to generate URLs to the


Pages/Customers/Index.cshtml page. For example:

Url.Page("./Index", ...)
<a asp-page="./Index">Customers Index Page</a>

RedirectToPage("./Index")
The absolute page name /Index is used to generate URLs to the Pages/Index.cshtml
page. For example:

Url.Page("/Index", ...)

<a asp-page="/Index">Home Index Page</a>


RedirectToPage("/Index")

The page name is the path to the page from the root /Pages folder including a leading
/ (for example, /Index ). The preceding URL generation samples offer enhanced options
and functional capabilities over hard-coding a URL. URL generation uses routing and
can generate and encode parameters according to how the route is defined in the
destination path.

URL generation for pages supports relative names. The following table shows which
Index page is selected using different RedirectToPage parameters in
Pages/Customers/Create.cshtml .

RedirectToPage(x) Page

RedirectToPage("/Index") Pages/Index

RedirectToPage("./Index"); Pages/Customers/Index

RedirectToPage("../Index") Pages/Index

RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index") , RedirectToPage("./Index") , and RedirectToPage("../Index")


are relative names. The RedirectToPage parameter is combined with the path of the
current page to compute the name of the destination page.

Relative name linking is useful when building sites with a complex structure. When
relative names are used to link between pages in a folder:

Renaming a folder doesn't break the relative links.


Links are not broken because they don't include the folder name.

To redirect to a page in a different Area, specify the area:

C#

RedirectToPage("/Index", new { area = "Services" });

For more information, see Areas in ASP.NET Core and Razor Pages route and app
conventions in ASP.NET Core.
ViewData attribute
Data can be passed to a page with ViewDataAttribute. Properties with the [ViewData]
attribute have their values stored and loaded from the ViewDataDictionary.

In the following example, the AboutModel applies the [ViewData] attribute to the Title
property:

C#

public class AboutModel : PageModel


{
[ViewData]
public string Title { get; } = "About";

public void OnGet()


{
}
}

In the About page, access the Title property as a model property:

CSHTML

<h1>@Model.Title</h1>

In the layout, the title is read from the ViewData dictionary:

CSHTML

<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"] - WebApplication</title>
...

TempData
ASP.NET Core exposes the TempData. This property stores data until it's read. The Keep
and Peek methods can be used to examine the data without deletion. TempData is useful
for redirection, when data is needed for more than a single request.

The following code sets the value of Message using TempData :

C#
public class CreateDotModel : PageModel
{
private readonly AppDbContext _db;

public CreateDotModel(AppDbContext db)


{
_db = db;
}

[TempData]
public string Message { get; set; }

[BindProperty]
public Customer Customer { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";
return RedirectToPage("./Index");
}
}

The following markup in the Pages/Customers/Index.cshtml file displays the value of


Message using TempData .

CSHTML

<h3>Msg: @Model.Message</h3>

The Pages/Customers/Index.cshtml.cs page model applies the [TempData] attribute to


the Message property.

C#

[TempData]
public string Message { get; set; }

For more information, see TempData.

Multiple handlers per page


The following page generates markup for two handlers using the asp-page-handler Tag
Helper:

CSHTML

@page
@model CreateFATHModel

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<!-- <snippet_Handlers> -->
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC"
/>
<!-- </snippet_Handlers> -->
</form>
</body>
</html>

The form in the preceding example has two submit buttons, each using the
FormActionTagHelper to submit to a different URL. The asp-page-handler attribute is a

companion to asp-page . asp-page-handler generates URLs that submit to each of the


handler methods defined by a page. asp-page isn't specified because the sample is
linking to the current page.

The page model:

C#

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
public class CreateFATHModel : PageModel
{
private readonly AppDbContext _db;

public CreateFATHModel(AppDbContext db)


{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }

public async Task<IActionResult> OnPostJoinListAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}

public async Task<IActionResult> OnPostJoinListUCAsync()


{
if (!ModelState.IsValid)
{
return Page();
}
Customer.Name = Customer.Name?.ToUpperInvariant();
return await OnPostJoinListAsync();
}
}
}

The preceding code uses named handler methods. Named handler methods are created
by taking the text in the name after On<HTTP Verb> and before Async (if present). In the
preceding example, the page methods are OnPostJoinListAsync and
OnPostJoinListUCAsync. With OnPost and Async removed, the handler names are
JoinList and JoinListUC .

CSHTML

<input type="submit" asp-page-handler="JoinList" value="Join" />


<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

Using the preceding code, the URL path that submits to OnPostJoinListAsync is
https://localhost:5001/Customers/CreateFATH?handler=JoinList . The URL path that

submits to OnPostJoinListUCAsync is https://localhost:5001/Customers/CreateFATH?


handler=JoinListUC .

Custom routes
Use the @page directive to:
Specify a custom route to a page. For example, the route to the About page can be
set to /Some/Other/Path with @page "/Some/Other/Path" .
Append segments to a page's default route. For example, an "item" segment can
be added to a page's default route with @page "item" .
Append parameters to a page's default route. For example, an ID parameter, id ,
can be required for a page with @page "{id}" .

A root-relative path designated by a tilde ( ~ ) at the beginning of the path is supported.


For example, @page "~/Some/Other/Path" is the same as @page "/Some/Other/Path" .

If you don't like the query string ?handler=JoinList in the URL, change the route to put
the handler name in the path portion of the URL. The route can be customized by
adding a route template enclosed in double quotes after the @page directive.

CSHTML

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC"
/>
</form>
</body>
</html>

Using the preceding code, the URL path that submits to OnPostJoinListAsync is
https://localhost:5001/Customers/CreateFATH/JoinList . The URL path that submits to
OnPostJoinListUCAsync is https://localhost:5001/Customers/CreateFATH/JoinListUC .

The ? following handler means the route parameter is optional.

Collocation of JavaScript (JS) files


Collocation of JavaScript (JS) files for pages, views, and Razor components is a
convenient way to organize scripts in an app.
Collocate JS files using the following filename extension conventions:

Pages of Razor Pages apps and views of MVC apps: .cshtml.js . Examples:
Pages/Index.cshtml.js for the Index page of a Razor Pages app at

Pages/Index.cshtml .
Views/Home/Index.cshtml.js for the Index view of an MVC app at

Views/Home/Index.cshtml .

Razor components of Blazor apps: .razor.js . Example: Pages/Index.razor.js for


the Index component at Pages/Index.razor .

Collocated JS files are publicly addressable using the path to the file in the project:

Pages, views, and components from a collocated scripts file in the app:

{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js

The {PATH} placeholder is the path to the page, view, or component.


The {PAGE, VIEW, OR COMPONENT} placeholder is the page, view, or component.
The {EXTENSION} placeholder matches the extension of the page, view, or
component, either razor or cshtml .

Razor Pages example:

A JS file for the Index page is placed in the Pages folder ( Pages/Index.cshtml.js )
next to the Index page ( Pages/Index.cshtml ). In the Index page, the script is
referenced at the path in the Pages folder:

razor

@section Scripts {
<script src="~/Pages/Index.cshtml.js"></script>
}

When the app is published, the framework automatically moves the script to the
web root. In the preceding example, the script is moved to bin\Release\{TARGET
FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.cshtml.js , where the {TARGET

FRAMEWORK MONIKER} placeholder is the Target Framework Moniker (TFM). No

change is required to the script's relative URL in the Index page.

Blazor example:

A JS file for the Index component is placed in the Pages folder


( Pages/Index.razor.js ) next to the Index component ( Pages/Index.razor ). In the
Index component, the script is referenced at the path in the Pages folder.
Pages/Index.razor.js :

JavaScript

export function showPrompt(message) {


return prompt(message, 'Type anything here');
}

In the OnAfterRenderAsync method of the Index component ( Pages/Index.razor ):

razor

module = await JS.InvokeAsync<IJSObjectReference>(


"import", "./Pages/Index.razor.js");

When the app is published, the framework automatically moves the script to the
web root. In the preceding example, the script is moved to bin\Release\{TARGET
FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.razor.js , where the {TARGET

FRAMEWORK MONIKER} placeholder is the Target Framework Moniker (TFM). No


change is required to the script's relative URL in the Index component.

For scripts provided by a Razor class library (RCL):

_content/{PACKAGE ID}/{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js


The {PACKAGE ID} placeholder is the RCL's package identifier (or library name
for a class library referenced by the app).
The {PATH} placeholder is the path to the page, view, or component. If a Razor
component is located at the root of the RCL, the path segment isn't included.
The {PAGE, VIEW, OR COMPONENT} placeholder is the page, view, or component.
The {EXTENSION} placeholder matches the extension of page, view, or
component, either razor or cshtml .

In the following Blazor app example:


The RCL's package identifier is AppJS .
A module's scripts are loaded for the Index component ( Index.razor ).
The Index component is in the Pages folder of the RCL.

C#

var module = await JS.InvokeAsync<IJSObjectReference>("import",


"./_content/AppJS/Pages/Index.razor.js");
Advanced configuration and settings
The configuration and settings in following sections is not required by most apps.

To configure advanced options, use the AddRazorPages overload that configures


RazorPagesOptions:

C#

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
options.RootDirectory = "/MyPages";
options.Conventions.AuthorizeFolder("/MyPages/Admin");
});

builder.Services.AddDbContext<CustomerDbContext>(options =>
options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Use the RazorPagesOptions to set the root directory for pages, or add application model
conventions for pages. For more information on conventions, see Razor Pages
authorization conventions.

To precompile views, see Razor view compilation.

Specify that Razor Pages are at the content root


By default, Razor Pages are rooted in the /Pages directory. Add
WithRazorPagesAtContentRoot to specify that your Razor Pages are at the content root
(ContentRootPath) of the app:

C#

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
.WithRazorPagesAtContentRoot();

builder.Services.AddDbContext<CustomerDbContext>(options =>
options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Specify that Razor Pages are at a custom root directory


Add WithRazorPagesRoot to specify that Razor Pages are at a custom root directory in
the app (provide a relative path):

C#

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages(options =>
{
options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
.WithRazorPagesRoot("/path/to/razor/pages");

builder.Services.AddDbContext<CustomerDbContext>(options =>
options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Additional resources
See Get started with Razor Pages, which builds on this introduction.
Authorize attribute and Razor Pages
Download or view sample code
Overview of ASP.NET Core
Razor syntax reference for ASP.NET Core
Areas in ASP.NET Core
Tutorial: Get started with Razor Pages in ASP.NET Core
Razor Pages authorization conventions in ASP.NET Core
Razor Pages route and app conventions in ASP.NET Core
Razor Pages unit tests in ASP.NET Core
Partial views in ASP.NET Core
Prerender and integrate ASP.NET Core Razor components
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
Filter methods for Razor Pages in
ASP.NET Core
Article • 04/11/2023

By Rick Anderson

Razor Page filters IPageFilter and IAsyncPageFilter allow Razor Pages to run code before
and after a Razor Page handler is run. Razor Page filters are similar to ASP.NET Core
MVC action filters, except they can't be applied to individual page handler methods.

Razor Page filters:

Run code after a handler method has been selected, but before model binding
occurs.
Run code before the handler method executes, after model binding is complete.
Run code after the handler method executes.
Can be implemented on a page or globally.
Cannot be applied to specific page handler methods.
Can have constructor dependencies populated by Dependency Injection (DI). For
more information, see ServiceFilterAttribute and TypeFilterAttribute.

While page constructors and middleware enable executing custom code before a
handler method executes, only Razor Page filters enable access to HttpContext and the
page. Middleware has access to the HttpContext , but not to the "page context". Filters
have a FilterContext derived parameter, which provides access to HttpContext . Here's a
sample for a page filter: Implement a filter attribute that adds a header to the response,
something that can't be done with constructors or middleware. Access to the page
context, which includes access to the instances of the page and it's model, are only
available when executing filters, handlers, or the body of a Razor Page.

View or download sample code (how to download)

Razor Page filters provide the following methods, which can be applied globally or at
the page level:

Synchronous methods:
OnPageHandlerSelected : Called after a handler method has been selected, but
before model binding occurs.
OnPageHandlerExecuting : Called before the handler method executes, after
model binding is complete.
OnPageHandlerExecuted : Called after the handler method executes, before the
action result.

Asynchronous methods:
OnPageHandlerSelectionAsync : Called asynchronously after the handler
method has been selected, but before model binding occurs.
OnPageHandlerExecutionAsync : Called asynchronously before the handler
method is invoked, after model binding is complete.

Implement either the synchronous or the async version of a filter interface, not both.
The framework checks first to see if the filter implements the async interface, and if so, it
calls that. If not, it calls the synchronous interface's method(s). If both interfaces are
implemented, only the async methods are called. The same rule applies to overrides in
pages, implement the synchronous or the async version of the override, not both.

Implement Razor Page filters globally


The following code implements IAsyncPageFilter :

C#

public class SampleAsyncPageFilter : IAsyncPageFilter


{
private readonly IConfiguration _config;

public SampleAsyncPageFilter(IConfiguration config)


{
_config = config;
}

public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext


context)
{
var key = _config["UserAgentID"];
context.HttpContext.Request.Headers.TryGetValue("user-agent",
out StringValues
value);
ProcessUserAgent.Write(context.ActionDescriptor.DisplayName,

"SampleAsyncPageFilter.OnPageHandlerSelectionAsync",
value, key.ToString());

return Task.CompletedTask;
}

public async Task


OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,

PageHandlerExecutionDelegate next)
{
// Do post work.
await next.Invoke();
}
}

In the preceding code, ProcessUserAgent.Write is user supplied code that works with
the user agent string.

The following code enables the SampleAsyncPageFilter in the Startup class:

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddRazorPages()
.AddMvcOptions(options =>
{
options.Filters.Add(new SampleAsyncPageFilter(Configuration));
});
}

The following code calls AddFolderApplicationModelConvention to apply the


SampleAsyncPageFilter to only pages in /Movies:

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddRazorPages(options =>
{
options.Conventions.AddFolderApplicationModelConvention(
"/Movies",
model => model.Filters.Add(new
SampleAsyncPageFilter(Configuration)));
});
}

The following code implements the synchronous IPageFilter :

C#

public class SamplePageFilter : IPageFilter


{
private readonly IConfiguration _config;

public SamplePageFilter(IConfiguration config)


{
_config = config;
}

public void OnPageHandlerSelected(PageHandlerSelectedContext context)


{
var key = _config["UserAgentID"];
context.HttpContext.Request.Headers.TryGetValue("user-agent", out
StringValues value);
ProcessUserAgent.Write(context.ActionDescriptor.DisplayName,
"SamplePageFilter.OnPageHandlerSelected",
value, key.ToString());
}

public void OnPageHandlerExecuting(PageHandlerExecutingContext context)


{
Debug.WriteLine("Global sync OnPageHandlerExecuting called.");
}

public void OnPageHandlerExecuted(PageHandlerExecutedContext context)


{
Debug.WriteLine("Global sync OnPageHandlerExecuted called.");
}
}

The following code enables the SamplePageFilter :

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddRazorPages()
.AddMvcOptions(options =>
{
options.Filters.Add(new SamplePageFilter(Configuration));
});
}

Implement Razor Page filters by overriding


filter methods
The following code overrides the asynchronous Razor Page filters:

C#

public class IndexModel : PageModel


{
private readonly IConfiguration _config;

public IndexModel(IConfiguration config)


{
_config = config;
}

public override Task


OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
Debug.WriteLine("/IndexModel OnPageHandlerSelectionAsync");
return Task.CompletedTask;
}

public async override Task


OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,

PageHandlerExecutionDelegate next)
{
var key = _config["UserAgentID"];
context.HttpContext.Request.Headers.TryGetValue("user-agent", out
StringValues value);
ProcessUserAgent.Write(context.ActionDescriptor.DisplayName,
"/IndexModel-OnPageHandlerExecutionAsync",
value, key.ToString());

await next.Invoke();
}
}

Implement a filter attribute


The built-in attribute-based filter OnResultExecutionAsync filter can be subclassed. The
following filter adds a header to the response:

C#

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace PageFilter.Filters
{
public class AddHeaderAttribute : ResultFilterAttribute
{
private readonly string _name;
private readonly string _value;

public AddHeaderAttribute (string name, string value)


{
_name = name;
_value = value;
}

public override void OnResultExecuting(ResultExecutingContext


context)
{
context.HttpContext.Response.Headers.Add(_name, new string[] {
_value });
}
}
}

The following code applies the AddHeader attribute:

C#

using Microsoft.AspNetCore.Mvc.RazorPages;
using PageFilter.Filters;

namespace PageFilter.Movies
{
[AddHeader("Author", "Rick")]
public class TestModel : PageModel
{
public void OnGet()
{

}
}
}

Use a tool such as the browser developer tools to examine the headers. Under Response
Headers, author: Rick is displayed.

See Overriding the default order for instructions on overriding the order.

See Cancellation and short circuiting for instructions to short-circuit the filter pipeline
from a filter.

Authorize filter attribute


The Authorize attribute can be applied to a PageModel :

C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace PageFilter.Pages
{
[Authorize]
public class ModelWithAuthFilterModel : PageModel
{
public IActionResult OnGet() => Page();
}
}
Razor Pages route and app conventions
in ASP.NET Core
Article • 06/04/2022

Learn how to use page route and app model provider conventions to control page
routing, discovery, and processing in Razor Pages apps.

To specify a page route, add route segments, or add parameters to a route, use the
page's @page directive. For more information, see Custom routes.

There are reserved words that can't be used as route segments or parameter names. For
more information, see Routing: Reserved routing names.

View or download sample code (how to download)

Scenario The sample demonstrates

Model conventions Add a route template and header to an app's


pages.
Conventions.Add

IPageRouteModelConvention
IPageApplicationModelConvention
IPageHandlerModelConvention

Page route action conventions Add a route template to pages in a folder and to
a single page.
AddFolderRouteModelConvention
AddPageRouteModelConvention
AddPageRoute

Page model action conventions Add a header to pages in a folder, add a header
to a single page, and configure a filter factory to
AddFolderApplicationModelConvention add a header to an app's pages.
AddPageApplicationModelConvention
ConfigureFilter (filter class, lambda
expression, or filter factory)

Razor Pages conventions are configured using an AddRazorPages overload that


configures RazorPagesOptions. The following convention examples are explained later in
this topic:

C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
options.Conventions.Add( ... );
options.Conventions.AddFolderRouteModelConvention(
"/OtherPages", model => { ... });
options.Conventions.AddPageRouteModelConvention(
"/About", model => { ... });
options.Conventions.AddPageRoute(
"/Contact", "TheContactPage/{text?}");
options.Conventions.AddFolderApplicationModelConvention(
"/OtherPages", model => { ... });
options.Conventions.AddPageApplicationModelConvention(
"/About", model => { ... });
options.Conventions.ConfigureFilter(model => { ... });
options.Conventions.ConfigureFilter( ... );
});
}

Route order
Routes specify an Order for processing (route matching).

Route Behavior
order

-1 The route is processed before other routes are processed.

0 Order isn't specified (default value). Not assigning Order ( Order = null ) defaults the
route Order to 0 (zero) for processing.

1, 2, … n Specifies the route processing order.

Route processing is established by convention:

Routes are processed in sequential order (-1, 0, 1, 2, … n).


When routes have the same Order , the most specific route is matched first
followed by less specific routes.
When routes with the same Order and the same number of parameters match a
request URL, routes are processed in the order that they're added to the
PageConventionCollection.

If possible, avoid depending on an established route processing order. Generally,


routing selects the correct route with URL matching. If you must set route Order
properties to route requests correctly, the app's routing scheme is probably confusing
to clients and fragile to maintain. Seek to simplify the app's routing scheme. The sample
app requires an explicit route processing order to demonstrate several routing scenarios
using a single app. However, you should attempt to avoid the practice of setting route
Order in production apps.

Razor Pages routing and MVC controller routing share an implementation. Information
on route order in the MVC topics is available at Routing to controller actions: Ordering
attribute routes.

Model conventions
Add a delegate for IPageConvention to add model conventions that apply to Razor
Pages.

Add a route model convention to all pages


Use Conventions to create and add an IPageRouteModelConvention to the collection of
IPageConvention instances that are applied during page route model construction.

The sample app contains the GlobalTemplatePageRouteModelConvention class to add a


{globalTemplate?} route template to all of the pages in the app:

C#

using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace SampleApp.Conventions;

public class GlobalTemplatePageRouteModelConvention :


IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 1,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel!.Template,
"{globalTemplate?}"),
}
});
}
}
}

In the preceding code:

The PageRouteModel is passed to the Apply method.


The PageRouteModel.Selectors gets the count of selectors.
A new SelectorModel is added which contains a AttributeRouteModel

Razor Pages options, such as adding Conventions, are added when Razor Pages is
added to the service collection. For an example, see the sample app .

C#

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.EntityFrameworkCore;
using SampleApp.Conventions;
using SampleApp.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>

options.UseInMemoryDatabase("InMemoryDb"));

builder.Services.AddRazorPages(options =>
{
options.Conventions.Add(new
GlobalTemplatePageRouteModelConvention());

options.Conventions.AddFolderRouteModelConvention("/OtherPages",
model =>
{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 2,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel!.Template,
"{otherPagesTemplate?}"),
}
});
}
});

options.Conventions.AddPageRouteModelConvention("/About", model =>


{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 2,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel!.Template,
"{aboutTemplate?}"),
}
});
}
});

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapRazorPages();
app.Run();

Consider the GlobalTemplatePageRouteModelConvention class:

C#

using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace SampleApp.Conventions;

public class GlobalTemplatePageRouteModelConvention :


IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 1,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel!.Template,
"{globalTemplate?}"),
}
});
}
}
}

The Order property for the AttributeRouteModel is set to 1 . This ensures the following
route matching behavior in the sample app:

A route template for TheContactPage/{text?} is added later in this topic. The


Contact Page route has a default order of null ( Order = 0 ), so it matches before
the {globalTemplate?} route template which has Order = 1 .

The {aboutTemplate?} route template is show in the preceding code. The


{aboutTemplate?} template is given an Order of 2 . When the About page is

requested at /About/RouteDataValue , "RouteDataValue" is loaded into


RouteData.Values["globalTemplate"] ( Order = 1 ) and not
RouteData.Values["aboutTemplate"] ( Order = 2 ) due to setting the Order property.

The {otherPagesTemplate?} route template is shown in the preceding code. The


{otherPagesTemplate?} template is given an Order of 2 . When any page in the

Pages/OtherPages folder is requested with a route parameter:

For example, /OtherPages/Page1/xyz

The route data value "xyz" is loaded into RouteData.Values["globalTemplate"]


( Order = 1 ).

RouteData.Values["otherPagesTemplate"] with ( Order = 2 ) is not loaded due to the

Order property 2 having a higher value.

When possible, don't set the Order . When Order is not set, it defaults to Order = 0 .
Rely on routing to select the correct route rather than the Order property.

Request the sample's About page at localhost:{port}/About/GlobalRouteValue and


inspect the result:
The sample app uses the Rick.Docs.Samples.RouteInfo NuGet package to display
routing information in the logging output. Using localhost:
{port}/About/GlobalRouteValue , the logger displays the request, the Order , and the

template used:

.NET CLI

info: SampleApp.Pages.AboutModel[0]
/About/GlobalRouteValue Order = 1 Template =
About/{globalTemplate?}

Add an app model convention to all pages


Use Conventions to create and add an IPageApplicationModelConvention to the
collection of IPageConvention instances that are applied during page app model
construction.

To demonstrate this and other conventions later in the topic, the sample app includes an
AddHeaderAttribute class. The class constructor accepts a name string and a values
string array. These values are used in its OnResultExecuting method to set a response
header. The full class is shown in the Page model action conventions section later in the
topic.

The sample app uses the AddHeaderAttribute class to add a header, GlobalHeader , to all
of the pages in the app:

C#

public class GlobalHeaderPageApplicationModelConvention


: IPageApplicationModelConvention
{
public void Apply(PageApplicationModel model)
{
model.Filters.Add(new AddHeaderAttribute(
"GlobalHeader", new string[] { "Global Header Value" }));
}
}

Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("InMemoryDb"));

builder.Services.AddRazorPages(options =>
{
options.Conventions.Add(new
GlobalTemplatePageRouteModelConvention());

options.Conventions.Add(new
GlobalHeaderPageApplicationModelConvention());

Request the sample's About page at localhost:{port}/About and inspect the headers to
view the result:

Add a handler model convention to all pages


Use Conventions to create and add an IPageHandlerModelConvention to the collection
of IPageConvention instances that are applied during page handler model construction.

C#

public class GlobalPageHandlerModelConvention


: IPageHandlerModelConvention
{
public void Apply(PageHandlerModel model)
{
// Access the PageHandlerModel
}
}

Page route action conventions


The default route model provider that derives from IPageRouteModelProvider invokes
conventions which are designed to provide extensibility points for configuring page
routes.

Folder route model convention


Use AddFolderRouteModelConvention to create and add an
IPageRouteModelConvention that invokes an action on the PageRouteModel for all of
the pages under the specified folder.

The sample app uses AddFolderRouteModelConvention to add an


{otherPagesTemplate?} route template to the pages in the OtherPages folder:

C#

options.Conventions.AddFolderRouteModelConvention("/OtherPages", model =>


{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 2,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel!.Template,
"{otherPagesTemplate?}"),
}
});
}
});

The Order property for the AttributeRouteModel is set to 2 . This ensures that the
template for {globalTemplate?} (set earlier in the topic to 1 ) is given priority for the first
route data value position when a single route value is provided. If a page in the
Pages/OtherPages folder is requested with a route parameter value (for example,
/OtherPages/Page1/RouteDataValue ), "RouteDataValue" is loaded into
RouteData.Values["globalTemplate"] ( Order = 1 ) and not

RouteData.Values["otherPagesTemplate"] ( Order = 2 ) due to setting the Order property.

Wherever possible, don't set the Order , which results in Order = 0 . Rely on routing to
select the correct route.

Request the sample's Page1 page at


localhost:5000/OtherPages/Page1/GlobalRouteValue/OtherPagesRouteValue and inspect

the result:

Page route model convention


Use AddPageRouteModelConvention to create and add an IPageRouteModelConvention
that invokes an action on the PageRouteModel for the page with the specified name.

The sample app uses AddPageRouteModelConvention to add an {aboutTemplate?} route


template to the About page:

C#

options.Conventions.AddPageRouteModelConvention("/About", model =>


{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = 2,
Template = AttributeRouteModel.CombineTemplates(
selector.AttributeRouteModel!.Template,
"{aboutTemplate?}"),
}
});
}
});

The Order property for the AttributeRouteModel is set to 2 . This ensures that the
template for {globalTemplate?} (set earlier in the topic to 1 ) is given priority for the first
route data value position when a single route value is provided. If the About page is
requested with a route parameter value at /About/RouteDataValue , "RouteDataValue" is
loaded into RouteData.Values["globalTemplate"] ( Order = 1 ) and not
RouteData.Values["aboutTemplate"] ( Order = 2 ) due to setting the Order property.

Wherever possible, don't set the Order , which results in Order = 0 . Rely on routing to
select the correct route.

Request the sample's About page at localhost:


{port}/About/GlobalRouteValue/AboutRouteValue and inspect the result:

The logger output displays:

.NET CLI

info: SampleApp.Pages.AboutModel[0]
/About/GlobalRouteValue/AboutRouteValue Order = 2 Template =
About/{globalTemplate?}/{aboutTemplate?}
Use a parameter transformer to customize
page routes
See Parameter transformers.

Configure a page route


Use AddPageRoute to configure a route to a page at the specified page path. Generated
links to the page use the specified route. AddPageRoute uses
AddPageRouteModelConvention to establish the route.

The sample app creates a route to /TheContactPage for the Contact Razor Page:

C#

options.Conventions.AddPageRoute("/Contact", "TheContactPage/{text?}");

The Contact page can also be reached at / Contact1` via its default route.

The sample app's custom route to the Contact page allows for an optional text route
segment ( {text?} ). The page also includes this optional segment in its @page directive
in case the visitor accesses the page at its /Contact route:

CSHTML

@page "{text?}"
@model ContactModel
@{
ViewData["Title"] = "Contact";
}

<h1>@ViewData["Title"]</h1>
<h2>@Model.Message</h2>

<address>
One Microsoft Way<br>
Redmond, WA 98052-6399<br>
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong> <a
href="mailto:Support@example.com">Support@example.com</a><br>
<strong>Marketing:</strong> <a
href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

<p>@Model.RouteDataTextTemplateValue</p>

Note that the URL generated for the Contact link in the rendered page reflects the
updated route:

Visit the Contact page at either its ordinary route, /Contact , or the custom route,
/TheContactPage . If you supply an additional text route segment, the page shows the

HTML-encoded segment that you provide:

Page model action conventions


The default page model provider that implements IPageApplicationModelProvider
invokes conventions which are designed to provide extensibility points for configuring
page models. These conventions are useful when building and modifying page
discovery and processing scenarios.

For the examples in this section, the sample app uses an AddHeaderAttribute class,
which is a ResultFilterAttribute, that applies a response header:

C#

public class AddHeaderAttribute : ResultFilterAttribute


{
private readonly string _name;
private readonly string[] _values;

public AddHeaderAttribute(string name, string[] values)


{
_name = name;
_values = values;
}

public override void OnResultExecuting(ResultExecutingContext context)


{
context.HttpContext.Response.Headers.Add(_name, _values);
base.OnResultExecuting(context);
}
}

Using conventions, the sample demonstrates how to apply the attribute to all of the
pages in a folder and to a single page.

Folder app model convention

Use AddFolderApplicationModelConvention to create and add an


IPageApplicationModelConvention that invokes an action on PageApplicationModel
instances for all pages under the specified folder.

The sample demonstrates the use of AddFolderApplicationModelConvention by adding a


header, OtherPagesHeader , to the pages inside the OtherPages folder of the app:

C#

options.Conventions.AddFolderApplicationModelConvention("/OtherPages", model
=>
{
model.Filters.Add(new AddHeaderAttribute(
"OtherPagesHeader", new string[] { "OtherPages Header Value" }));
});
Request the sample's Page1 page at localhost:5000/OtherPages/Page1 and inspect the
headers to view the result:

Page app model convention

Use AddPageApplicationModelConvention to create and add an


IPageApplicationModelConvention that invokes an action on the PageApplicationModel
for the page with the specified name.

The sample demonstrates the use of AddPageApplicationModelConvention by adding a


header, AboutHeader , to the About page:

C#

options.Conventions.AddPageApplicationModelConvention("/About", model =>


{
model.Filters.Add(new AddHeaderAttribute(
"AboutHeader", new string[] { "About Header Value" }));
});

Request the sample's About page at localhost:5000/About and inspect the headers to
view the result:

Configure a filter
ConfigureFilter configures the specified filter to apply. You can implement a filter class,
but the sample app shows how to implement a filter in a lambda expression, which is
implemented behind-the-scenes as a factory that returns a filter:

C#

options.Conventions.ConfigureFilter(model =>
{
if (model.RelativePath.Contains("OtherPages/Page2"))
{
return new AddHeaderAttribute(
"OtherPagesPage2Header",
new string[] { "OtherPages/Page2 Header Value" });
}
return new EmptyFilter();
});

The page app model is used to check the relative path for segments that lead to the
Page2 page in the OtherPages folder. If the condition passes, a header is added. If not,
the EmptyFilter is applied.

EmptyFilter is an Action filter. Since Action filters are ignored by Razor Pages, the

EmptyFilter has no effect as intended if the path doesn't contain OtherPages/Page2 .

Request the sample's Page2 page at localhost:5000/OtherPages/Page2 and inspect the


headers to view the result:

Configure a filter factory

ConfigureFilter configures the specified factory to apply filters to all Razor Pages.

The sample app provides an example of using a filter factory by adding a header,
FilterFactoryHeader , with two values to the app's pages:

C#
options.Conventions.ConfigureFilter(new AddHeaderWithFactory());

AddHeaderWithFactory.cs :

C#

public class AddHeaderWithFactory : IFilterFactory


{
// Implement IFilterFactory
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return new AddHeaderFilter();
}

private class AddHeaderFilter : IResultFilter


{
public void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.Headers.Add(
"FilterFactoryHeader",
new string[]
{
"Filter Factory Header Value 1",
"Filter Factory Header Value 2"
});
}

public void OnResultExecuted(ResultExecutedContext context)


{
}
}

public bool IsReusable


{
get
{
return false;
}
}
}

Request the sample's About page at localhost:5000/About and inspect the headers to
view the result:
MVC Filters and the Page filter (IPageFilter)
MVC Action filters are ignored by Razor Pages, since Razor Pages use handler methods.
Other types of MVC filters are available for you to use: Authorization, Exception,
Resource, and Result. For more information, see the Filters topic.

The Page filter (IPageFilter) is a filter that applies to Razor Pages. For more information,
see Filter methods for Razor Pages.

Additional resources
Razor Pages Routing
Razor Pages authorization conventions in ASP.NET Core
Areas in ASP.NET Core
Overview of ASP.NET Core MVC
Article • 06/27/2022

By Steve Smith

ASP.NET Core MVC is a rich framework for building web apps and APIs using the Model-
View-Controller design pattern.

MVC pattern
The Model-View-Controller (MVC) architectural pattern separates an application into
three main groups of components: Models, Views, and Controllers. This pattern helps to
achieve separation of concerns. Using this pattern, user requests are routed to a
Controller which is responsible for working with the Model to perform user actions
and/or retrieve results of queries. The Controller chooses the View to display to the user,
and provides it with any Model data it requires.

The following diagram shows the three main components and which ones reference the
others:

This delineation of responsibilities helps you scale the application in terms of complexity
because it's easier to code, debug, and test something (model, view, or controller) that
has a single job. It's more difficult to update, test, and debug code that has
dependencies spread across two or more of these three areas. For example, user
interface logic tends to change more frequently than business logic. If presentation code
and business logic are combined in a single object, an object containing business logic
must be modified every time the user interface is changed. This often introduces errors
and requires the retesting of business logic after every minimal user interface change.

7 Note

Both the view and the controller depend on the model. However, the model
depends on neither the view nor the controller. This is one of the key benefits of
the separation. This separation allows the model to be built and tested
independent of the visual presentation.

Model Responsibilities
The Model in an MVC application represents the state of the application and any
business logic or operations that should be performed by it. Business logic should be
encapsulated in the model, along with any implementation logic for persisting the state
of the application. Strongly-typed views typically use ViewModel types designed to
contain the data to display on that view. The controller creates and populates these
ViewModel instances from the model.

View Responsibilities
Views are responsible for presenting content through the user interface. They use the
Razor view engine to embed .NET code in HTML markup. There should be minimal logic
within views, and any logic in them should relate to presenting content. If you find the
need to perform a great deal of logic in view files in order to display data from a
complex model, consider using a View Component, ViewModel, or view template to
simplify the view.

Controller Responsibilities
Controllers are the components that handle user interaction, work with the model, and
ultimately select a view to render. In an MVC application, the view only displays
information; the controller handles and responds to user input and interaction. In the
MVC pattern, the controller is the initial entry point, and is responsible for selecting
which model types to work with and which view to render (hence its name - it controls
how the app responds to a given request).

7 Note
Controllers shouldn't be overly complicated by too many responsibilities. To keep
controller logic from becoming overly complex, push business logic out of the
controller and into the domain model.

 Tip

If you find that your controller actions frequently perform the same kinds of
actions, move these common actions into filters.

ASP.NET Core MVC


The ASP.NET Core MVC framework is a lightweight, open source, highly testable
presentation framework optimized for use with ASP.NET Core.

ASP.NET Core MVC provides a patterns-based way to build dynamic websites that
enables a clean separation of concerns. It gives you full control over markup, supports
TDD-friendly development and uses the latest web standards.

Routing
ASP.NET Core MVC is built on top of ASP.NET Core's routing, a powerful URL-mapping
component that lets you build applications that have comprehensible and searchable
URLs. This enables you to define your application's URL naming patterns that work well
for search engine optimization (SEO) and for link generation, without regard for how the
files on your web server are organized. You can define your routes using a convenient
route template syntax that supports route value constraints, defaults and optional
values.

Convention-based routing enables you to globally define the URL formats that your
application accepts and how each of those formats maps to a specific action method on
a given controller. When an incoming request is received, the routing engine parses the
URL and matches it to one of the defined URL formats, and then calls the associated
controller's action method.

C#

routes.MapRoute(name: "Default", template: "


{controller=Home}/{action=Index}/{id?}");
Attribute routing enables you to specify routing information by decorating your
controllers and actions with attributes that define your application's routes. This means
that your route definitions are placed next to the controller and action with which
they're associated.

C#

[Route("api/[controller]")]
public class ProductsController : Controller
{
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
...
}
}

Model binding
ASP.NET Core MVC model binding converts client request data (form values, route data,
query string parameters, HTTP headers) into objects that the controller can handle. As a
result, your controller logic doesn't have to do the work of figuring out the incoming
request data; it simply has the data as parameters to its action methods.

C#

public async Task<IActionResult> Login(LoginViewModel model, string


returnUrl = null) { ... }

Model validation
ASP.NET Core MVC supports validation by decorating your model object with data
annotation validation attributes. The validation attributes are checked on the client side
before values are posted to the server, as well as on the server before the controller
action is called.

C#

using System.ComponentModel.DataAnnotations;
public class LoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }

[Display(Name = "Remember me?")]


public bool RememberMe { get; set; }
}

A controller action:

C#

public async Task<IActionResult> Login(LoginViewModel model, string


returnUrl = null)
{
if (ModelState.IsValid)
{
// work with the model
}
// At this point, something failed, redisplay form
return View(model);
}

The framework handles validating request data both on the client and on the server.
Validation logic specified on model types is added to the rendered views as unobtrusive
annotations and is enforced in the browser with jQuery Validation .

Dependency injection
ASP.NET Core has built-in support for dependency injection (DI). In ASP.NET Core MVC,
controllers can request needed services through their constructors, allowing them to
follow the Explicit Dependencies Principle.

Your app can also use dependency injection in view files, using the @inject directive:

CSHTML

@inject SomeService ServiceName

<!DOCTYPE html>
<html lang="en">
<head>
<title>@ServiceName.GetTitle</title>
</head>
<body>
<h1>@ServiceName.GetTitle</h1>
</body>
</html>

Filters
Filters help developers encapsulate cross-cutting concerns, like exception handling or
authorization. Filters enable running custom pre- and post-processing logic for action
methods, and can be configured to run at certain points within the execution pipeline
for a given request. Filters can be applied to controllers or actions as attributes (or can
be run globally). Several filters (such as Authorize ) are included in the framework.
[Authorize] is the attribute that is used to create MVC authorization filters.

C#

[Authorize]
public class AccountController : Controller

Areas
Areas provide a way to partition a large ASP.NET Core MVC Web app into smaller
functional groupings. An area is an MVC structure inside an application. In an MVC
project, logical components like Model, Controller, and View are kept in different
folders, and MVC uses naming conventions to create the relationship between these
components. For a large app, it may be advantageous to partition the app into separate
high level areas of functionality. For instance, an e-commerce app with multiple business
units, such as checkout, billing, and search etc. Each of these units have their own logical
component views, controllers, and models.

Web APIs
In addition to being a great platform for building web sites, ASP.NET Core MVC has
great support for building Web APIs. You can build services that reach a broad range of
clients including browsers and mobile devices.

The framework includes support for HTTP content-negotiation with built-in support to
format data as JSON or XML. Write custom formatters to add support for your own
formats.

Use link generation to enable support for hypermedia. Easily enable support for cross-
origin resource sharing (CORS) so that your Web APIs can be shared across multiple
Web applications.

Testability
The framework's use of interfaces and dependency injection make it well-suited to unit
testing, and the framework includes features (like a TestHost and InMemory provider for
Entity Framework) that make integration tests quick and easy as well. Learn more about
how to test controller logic.

Razor view engine


ASP.NET Core MVC views use the Razor view engine to render views. Razor is a compact,
expressive and fluid template markup language for defining views using embedded C#
code. Razor is used to dynamically generate web content on the server. You can cleanly
mix server code with client side content and code.

CSHTML

<ul>
@for (int i = 0; i < 5; i++) {
<li>List item @i</li>
}
</ul>

Using the Razor view engine you can define layouts, partial views and replaceable
sections.

Strongly typed views


Razor views in MVC can be strongly typed based on your model. Controllers can pass a
strongly typed model to views enabling your views to have type checking and
IntelliSense support.

For example, the following view renders a model of type IEnumerable<Product> :

CSHTML

@model IEnumerable<Product>
<ul>
@foreach (Product p in Model)
{
<li>@p.Name</li>
}
</ul>

Tag Helpers
Tag Helpers enable server side code to participate in creating and rendering HTML
elements in Razor files. You can use tag helpers to define custom tags (for example,
<environment> ) or to modify the behavior of existing tags (for example, <label> ). Tag
Helpers bind to specific elements based on the element name and its attributes. They
provide the benefits of server-side rendering while still preserving an HTML editing
experience.

There are many built-in Tag Helpers for common tasks - such as creating forms, links,
loading assets and more - and even more available in public GitHub repositories and as
NuGet packages. Tag Helpers are authored in C#, and they target HTML elements based
on element name, attribute name, or parent tag. For example, the built-in LinkTagHelper
can be used to create a link to the Login action of the AccountsController :

CSHTML

<p>
Thank you for confirming your email.
Please <a asp-controller="Account" asp-action="Login">Click here to Log
in</a>.
</p>

The EnvironmentTagHelper can be used to include different scripts in your views (for
example, raw or minified) based on the runtime environment, such as Development,
Staging, or Production:

CSHTML

<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.js"
asp-fallback-src="~/lib/jquery/dist/jquery.js"
asp-fallback-test="window.jQuery">
</script>
</environment>

Tag Helpers provide an HTML-friendly development experience and a rich IntelliSense


environment for creating HTML and Razor markup. Most of the built-in Tag Helpers
target existing HTML elements and provide server-side attributes for the element.

View Components
View Components allow you to package rendering logic and reuse it throughout the
application. They're similar to partial views, but with associated logic.

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.

Additional resources
MyTested.AspNetCore.Mvc - Fluent Testing Library for ASP.NET Core MVC :
Strongly-typed unit testing library, providing a fluent interface for testing MVC and
web API apps. (Not maintained or supported by Microsoft.)
Prerender and integrate ASP.NET Core Razor components
Dependency injection in ASP.NET Core
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
Views in ASP.NET Core MVC
Article • 06/03/2022

By Steve Smith and Dave Brock

This document explains views used in ASP.NET Core MVC applications. For information
on Razor Pages, see Introduction to Razor Pages in ASP.NET Core.

In the Model-View-Controller (MVC) pattern, the view handles the app's data
presentation and user interaction. A view is an HTML template with embedded Razor
markup. Razor markup is code that interacts with HTML markup to produce a webpage
that's sent to the client.

In ASP.NET Core MVC, views are .cshtml files that use the C# programming language in
Razor markup. Usually, view files are grouped into folders named for each of the app's
controllers. The folders are stored in a Views folder at the root of the app:

The Home controller is represented by a Home folder inside the Views folder. The Home
folder contains the views for the About , Contact , and Index (homepage) webpages.
When a user requests one of these three webpages, controller actions in the Home
controller determine which of the three views is used to build and return a webpage to
the user.

Use layouts to provide consistent webpage sections and reduce code repetition. Layouts
often contain the header, navigation and menu elements, and the footer. The header
and footer usually contain boilerplate markup for many metadata elements and links to
script and style assets. Layouts help you avoid this boilerplate markup in your views.

Partial views reduce code duplication by managing reusable parts of views. For example,
a partial view is useful for an author biography on a blog website that appears in several
views. An author biography is ordinary view content and doesn't require code to
execute in order to produce the content for the webpage. Author biography content is
available to the view by model binding alone, so using a partial view for this type of
content is ideal.

View components are similar to partial views in that they allow you to reduce repetitive
code, but they're appropriate for view content that requires code to run on the server in
order to render the webpage. View components are useful when the rendered content
requires database interaction, such as for a website shopping cart. View components
aren't limited to model binding in order to produce webpage output.

Benefits of using views


Views help to establish separation of concerns within an MVC app by separating the
user interface markup from other parts of the app. Following SoC design makes your
app modular, which provides several benefits:

The app is easier to maintain because it's better organized. Views are generally
grouped by app feature. This makes it easier to find related views when working on
a feature.
The parts of the app are loosely coupled. You can build and update the app's views
separately from the business logic and data access components. You can modify
the views of the app without necessarily having to update other parts of the app.
It's easier to test the user interface parts of the app because the views are separate
units.
Due to better organization, it's less likely that you'll accidentally repeat sections of
the user interface.

Creating a view
Views that are specific to a controller are created in the Views/[ControllerName] folder.
Views that are shared among controllers are placed in the Views/Shared folder. To create
a view, add a new file and give it the same name as its associated controller action with
the .cshtml file extension. To create a view that corresponds with the About action in
the Home controller, create an About.cshtml file in the Views/Home folder:

CSHTML

@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
<p>Use this area to provide additional information.</p>

Razor markup starts with the @ symbol. Run C# statements by placing C# code within
Razor code blocks set off by curly braces ( { ... } ). For example, see the assignment of
"About" to ViewData["Title"] shown above. You can display values within HTML by
simply referencing the value with the @ symbol. See the contents of the <h2> and <h3>
elements above.

The view content shown above is only part of the entire webpage that's rendered to the
user. The rest of the page's layout and other common aspects of the view are specified
in other view files. To learn more, see the Layout topic.

How controllers specify views


Views are typically returned from actions as a ViewResult, which is a type of
ActionResult. Your action method can create and return a ViewResult directly, but that
isn't commonly done. Since most controllers inherit from Controller, you simply use the
View helper method to return the ViewResult :

HomeController.cs :

C#

public IActionResult About()


{
ViewData["Message"] = "Your application description page.";

return View();
}

When this action returns, the About.cshtml view shown in the last section is rendered as
the following webpage:
The View helper method has several overloads. You can optionally specify:

An explicit view to return:

C#

return View("Orders");

A model to pass to the view:

C#

return View(Orders);

Both a view and a model:

C#

return View("Orders", Orders);

View discovery
When an action returns a view, a process called view discovery takes place. This process
determines which view file is used based on the view name.

The default behavior of the View method ( return View(); ) is to return a view with the
same name as the action method from which it's called. For example, the About
ActionResult method name of the controller is used to search for a view file named

About.cshtml . First, the runtime looks in the Views/[ControllerName] folder for the view.

If it doesn't find a matching view there, it searches the Shared folder for the view.
It doesn't matter if you implicitly return the ViewResult with return View(); or explicitly
pass the view name to the View method with return View("<ViewName>"); . In both
cases, view discovery searches for a matching view file in this order:

1. Views/\[ControllerName]/\[ViewName].cshtml
2. Views/Shared/\[ViewName].cshtml

A view file path can be provided instead of a view name. If using an absolute path
starting at the app root (optionally starting with "/" or "~/"), the .cshtml extension must
be specified:

C#

return View("Views/Home/About.cshtml");

You can also use a relative path to specify views in different directories without the
.cshtml extension. Inside the HomeController , you can return the Index view of your

Manage views with a relative path:

C#

return View("../Manage/Index");

Similarly, you can indicate the current controller-specific directory with the "./" prefix:

C#

return View("./About");

Partial views and view components use similar (but not identical) discovery mechanisms.

You can customize the default convention for how views are located within the app by
using a custom IViewLocationExpander.

View discovery relies on finding view files by file name. If the underlying file system is
case sensitive, view names are probably case sensitive. For compatibility across
operating systems, match case between controller and action names and associated
view folders and file names. If you encounter an error that a view file can't be found
while working with a case-sensitive file system, confirm that the casing matches
between the requested view file and the actual view file name.

Follow the best practice of organizing the file structure for your views to reflect the
relationships among controllers, actions, and views for maintainability and clarity.
Pass data to views
Pass data to views using several approaches:

Strongly typed data: viewmodel


Weakly typed data
ViewData ( ViewDataAttribute )

ViewBag

Strongly-typed data (viewmodel)


The most robust approach is to specify a model type in the view. This model is
commonly referred to as a viewmodel. You pass an instance of the viewmodel type to
the view from the action.

Using a viewmodel to pass data to a view allows the view to take advantage of strong
type checking. Strong typing (or strongly typed) means that every variable and constant
has an explicitly defined type (for example, string , int , or DateTime ). The validity of
types used in a view is checked at compile time.

Visual Studio and Visual Studio Code list strongly typed class members using a
feature called IntelliSense. When you want to see the properties of a viewmodel, type
the variable name for the viewmodel followed by a period ( . ). This helps you write code
faster with fewer errors.

Specify a model using the @model directive. Use the model with @Model :

CSHTML

@model WebApplication1.ViewModels.Address

<h2>Contact</h2>
<address>
@Model.Street<br>
@Model.City, @Model.State @Model.PostalCode<br>
<abbr title="Phone">P:</abbr> 425.555.0100
</address>

To provide the model to the view, the controller passes it as a parameter:

C#

public IActionResult Contact()


{
ViewData["Message"] = "Your contact page.";
var viewModel = new Address()
{
Name = "Microsoft",
Street = "One Microsoft Way",
City = "Redmond",
State = "WA",
PostalCode = "98052-6399"
};

return View(viewModel);
}

There are no restrictions on the model types that you can provide to a view. We
recommend using Plain Old CLR Object (POCO) viewmodels with little or no behavior
(methods) defined. Usually, viewmodel classes are either stored in the Models folder or a
separate ViewModels folder at the root of the app. The Address viewmodel used in the
example above is a POCO viewmodel stored in a file named Address.cs :

C#

namespace WebApplication1.ViewModels
{
public class Address
{
public string Name { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
}
}

Nothing prevents you from using the same classes for both your viewmodel types and
your business model types. However, using separate models allows your views to vary
independently from the business logic and data access parts of your app. Separation of
models and viewmodels also offers security benefits when models use model binding
and validation for data sent to the app by the user.

Weakly typed data ( ViewData , [ViewData] attribute, and


ViewBag )

ViewBag isn't available by default for use in Razor Pages PageModel classes.

In addition to strongly typed views, views have access to a weakly typed (also called
loosely typed) collection of data. Unlike strong types, weak types (or loose types) means
that you don't explicitly declare the type of data you're using. You can use the collection
of weakly typed data for passing small amounts of data in and out of controllers and
views.

Passing data between a Example


...

Controller and a view Populating a dropdown list with data.

View and a layout view Setting the <title> element content in the layout view from a view
file.

Partial view and a view A widget that displays data based on the webpage that the user
requested.

This collection can be referenced through either the ViewData or ViewBag properties on
controllers and views. The ViewData property is a dictionary of weakly typed objects. The
ViewBag property is a wrapper around ViewData that provides dynamic properties for

the underlying ViewData collection. Note: Key lookups are case-insensitive for both
ViewData and ViewBag .

ViewData and ViewBag are dynamically resolved at runtime. Since they don't offer
compile-time type checking, both are generally more error-prone than using a
viewmodel. For that reason, some developers prefer to minimally or never use ViewData
and ViewBag .

ViewData

ViewData is a ViewDataDictionary object accessed through string keys. String data can
be stored and used directly without the need for a cast, but you must cast other
ViewData object values to specific types when you extract them. You can use ViewData
to pass data from controllers to views and within views, including partial views and
layouts.

The following is an example that sets values for a greeting and an address using
ViewData in an action:

C#

public IActionResult SomeAction()


{
ViewData["Greeting"] = "Hello";
ViewData["Address"] = new Address()
{
Name = "Steve",
Street = "123 Main St",
City = "Hudson",
State = "OH",
PostalCode = "44236"
};

return View();
}

Work with the data in a view:

CSHTML

@{
// Since Address isn't a string, it requires a cast.
var address = ViewData["Address"] as Address;
}

@ViewData["Greeting"] World!

<address>
@address.Name<br>
@address.Street<br>
@address.City, @address.State @address.PostalCode
</address>

[ViewData] attribute

Another approach that uses the ViewDataDictionary is ViewDataAttribute. Properties on


controllers or Razor Page models marked with the [ViewData] attribute have their
values stored and loaded from the dictionary.

In the following example, the Home controller contains a Title property marked with
[ViewData] . The About method sets the title for the About view:

C#

public class HomeController : Controller


{
[ViewData]
public string Title { get; set; }

public IActionResult About()


{
Title = "About Us";
ViewData["Message"] = "Your application description page.";

return View();
}
}

In the layout, the title is read from the ViewData dictionary:

CSHTML

<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"] - WebApplication</title>
...

ViewBag

ViewBag isn't available by default for use in Razor Pages PageModel classes.

ViewBag is a Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.DynamicViewData object


that provides dynamic access to the objects stored in ViewData . ViewBag can be more
convenient to work with, since it doesn't require casting. The following example shows
how to use ViewBag with the same result as using ViewData above:

C#

public IActionResult SomeAction()


{
ViewBag.Greeting = "Hello";
ViewBag.Address = new Address()
{
Name = "Steve",
Street = "123 Main St",
City = "Hudson",
State = "OH",
PostalCode = "44236"
};

return View();
}

CSHTML

@ViewBag.Greeting World!

<address>
@ViewBag.Address.Name<br>
@ViewBag.Address.Street<br>
@ViewBag.Address.City, @ViewBag.Address.State
@ViewBag.Address.PostalCode
</address>

Using ViewData and ViewBag simultaneously


ViewBag isn't available by default for use in Razor Pages PageModel classes.

Since ViewData and ViewBag refer to the same underlying ViewData collection, you can
use both ViewData and ViewBag and mix and match between them when reading and
writing values.

Set the title using ViewBag and the description using ViewData at the top of an
About.cshtml view:

CSHTML

@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.Title = "About Contoso";
ViewData["Description"] = "Let us tell you about Contoso's philosophy
and mission.";
}

Read the properties but reverse the use of ViewData and ViewBag . In the _Layout.cshtml
file, obtain the title using ViewData and obtain the description using ViewBag :

CSHTML

<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"]</title>
<meta name="description" content="@ViewBag.Description">
...

Remember that strings don't require a cast for ViewData . You can use
@ViewData["Title"] without casting.

Using both ViewData and ViewBag at the same time works, as does mixing and matching
reading and writing the properties. The following markup is rendered:

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<title>About Contoso</title>
<meta name="description" content="Let us tell you about Contoso's
philosophy and mission.">
...

Summary of the differences between ViewData and ViewBag


ViewBag isn't available by default for use in Razor Pages PageModel classes.

ViewData

Derives from ViewDataDictionary, so it has dictionary properties that can be


useful, such as ContainsKey , Add , Remove , and Clear .
Keys in the dictionary are strings, so whitespace is allowed. Example:
ViewData["Some Key With Whitespace"]

Any type other than a string must be cast in the view to use ViewData .
ViewBag
Derives from
Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.DynamicViewData , so it allows
the creation of dynamic properties using dot notation ( @ViewBag.SomeKey =
<value or object> ), and no casting is required. The syntax of ViewBag makes it

quicker to add to controllers and views.


Simpler to check for null values. Example: @ViewBag.Person?.Name

When to use ViewData or ViewBag


Both ViewData and ViewBag are equally valid approaches for passing small amounts of
data among controllers and views. The choice of which one to use is based on
preference. You can mix and match ViewData and ViewBag objects, however, the code is
easier to read and maintain with one approach used consistently. Both approaches are
dynamically resolved at runtime and thus prone to causing runtime errors. Some
development teams avoid them.

Dynamic views
Views that don't declare a model type using @model but that have a model instance
passed to them (for example, return View(Address); ) can reference the instance's
properties dynamically:

CSHTML
<address>
@Model.Street<br>
@Model.City, @Model.State @Model.PostalCode<br>
<abbr title="Phone">P:</abbr> 425.555.0100
</address>

This feature offers flexibility but doesn't offer compilation protection or IntelliSense. If
the property doesn't exist, webpage generation fails at runtime.

More view features


Tag Helpers make it easy to add server-side behavior to existing HTML tags. Using Tag
Helpers avoids the need to write custom code or helpers within your views. Tag helpers
are applied as attributes to HTML elements and are ignored by editors that can't process
them. This allows you to edit and render view markup in a variety of tools.

Generating custom HTML markup can be achieved with many built-in HTML Helpers.
More complex user interface logic can be handled by View Components. View
components provide the same SoC that controllers and views offer. They can eliminate
the need for actions and views that deal with data used by common user interface
elements.

Like many other aspects of ASP.NET Core, views support dependency injection, allowing
services to be injected into views.

CSS isolation
Isolate CSS styles to individual pages, views, and components to reduce or avoid:

Dependencies on global styles that can be challenging to maintain.


Style conflicts in nested content.

To add a scoped CSS file for a page or view, place the CSS styles in a companion
.cshtml.css file matching the name of the .cshtml file. In the following example, an

Index.cshtml.css file supplies CSS styles that are only applied to the Index.cshtml page
or view.

Pages/Index.cshtml.css (Razor Pages) or Views/Index.cshtml.css (MVC):

css

h1 {
color: red;
}

CSS isolation occurs at build time. The framework rewrites CSS selectors to match
markup rendered by the app's pages or views. The rewritten CSS styles are bundled and
produced as a static asset, {APP ASSEMBLY}.styles.css . The placeholder {APP ASSEMBLY}
is the assembly name of the project. A link to the bundled CSS styles is placed in the
app's layout.

In the <head> content of the app's Pages/Shared/_Layout.cshtml (Razor Pages) or


Views/Shared/_Layout.cshtml (MVC), add or confirm the presence of the link to the
bundled CSS styles:

HTML

<link rel="stylesheet" href="~/{APP ASSEMBLY}.styles.css" />

In the following example, the app's assembly name is WebApp :

HTML

<link rel="stylesheet" href="WebApp.styles.css" />

The styles defined in a scoped CSS file are only applied to the rendered output of the
matching file. In the preceding example, any h1 CSS declarations defined elsewhere in
the app don't conflict with the Index 's heading style. CSS style cascading and
inheritance rules remain in effect for scoped CSS files. For example, styles applied
directly to an <h1> element in the Index.cshtml file override the scoped CSS file's styles
in Index.cshtml.css .

7 Note

In order to guarantee CSS style isolation when bundling occurs, importing CSS in
Razor code blocks isn't supported.

CSS isolation only applies to HTML elements. CSS isolation isn't supported for Tag
Helpers.

Within the bundled CSS file, each page, view, or Razor component is associated with a
scope identifier in the format b-{STRING} , where the {STRING} placeholder is a ten-
character string generated by the framework. The following example provides the style
for the preceding <h1> element in the Index page of a Razor Pages app:
css

/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
color: red;
}

In the Index page where the CSS style is applied from the bundled file, the scope
identifier is appended as an HTML attribute:

HTML

<h1 b-3xxtam6d07>

The identifier is unique to an app. At build time, a project bundle is created with the
convention {STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css , where the placeholder
{STATIC WEB ASSETS BASE PATH} is the static web assets base path.

If other projects are utilized, such as NuGet packages or Razor class libraries, the
bundled file:

References the styles using CSS imports.


Isn't published as a static web asset of the app that consumes the styles.

CSS preprocessor support


CSS preprocessors are useful for improving CSS development by utilizing features such
as variables, nesting, modules, mixins, and inheritance. While CSS isolation doesn't
natively support CSS preprocessors such as Sass or Less, integrating CSS preprocessors
is seamless as long as preprocessor compilation occurs before the framework rewrites
the CSS selectors during the build process. Using Visual Studio for example, configure
existing preprocessor compilation as a Before Build task in the Visual Studio Task
Runner Explorer.

Many third-party NuGet packages, such as AspNetCore.SassCompiler , can compile


SASS/SCSS files at the beginning of the build process before CSS isolation occurs, and
no additional configuration is required.

CSS isolation configuration


CSS isolation permits configuration for some advanced scenarios, such as when there
are dependencies on existing tools or workflows.
Customize scope identifier format
In this section, the {Pages|Views} placeholder is either Pages for Razor Pages apps or
Views for MVC apps.

By default, scope identifiers use the format b-{STRING} , where the {STRING} placeholder
is a ten-character string generated by the framework. To customize the scope identifier
format, update the project file to a desired pattern:

XML

<ItemGroup>
<None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-
identifier" />
</ItemGroup>

In the preceding example, the CSS generated for Index.cshtml.css changes its scope
identifier from b-{STRING} to custom-scope-identifier .

Use scope identifiers to achieve inheritance with scoped CSS files. In the following
project file example, a BaseView.cshtml.css file contains common styles across views. A
DerivedView.cshtml.css file inherits these styles.

XML

<ItemGroup>
<None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-
identifier" />
<None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-
scope-identifier" />
</ItemGroup>

Use the wildcard ( * ) operator to share scope identifiers across multiple files:

XML

<ItemGroup>
<None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-
identifier" />
</ItemGroup>

Change base path for static web assets


The scoped CSS file is generated at the root of the app. In the project file, use the
StaticWebAssetBasePath property to change the default path. The following example
places the scoped CSS file, and the rest of the app's assets, at the _content path:

XML

<PropertyGroup>
<StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>

Disable automatic bundling


To opt out of how framework publishes and loads scoped files at runtime, use the
DisableScopedCssBundling property. When using this property, other tools or processes

are responsible for taking the isolated CSS files from the obj directory and publishing
and loading them at runtime:

XML

<PropertyGroup>
<DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>

Razor class library (RCL) support


When a Razor class library (RCL) provides isolated styles, the <link> tag's href attribute
points to {STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css , where the
placeholders are:

{STATIC WEB ASSET BASE PATH} : The static web asset base path.

{PACKAGE ID} : The library's package identifier. The package identifier defaults to
the project's assembly name if the package identifier isn't specified in the project
file.

In the following example:

The static web asset base path is _content/ClassLib .


The class library's assembly name is ClassLib .

Pages/Shared/_Layout.cshtml (Razor Pages) or Views/Shared/_Layout.cshtml (MVC):

HTML
<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">

For more information on RCLs, see the following articles:

Reusable Razor UI in class libraries with ASP.NET Core


Consume ASP.NET Core Razor components from a Razor class library (RCL)

For information on Blazor CSS isolation, see ASP.NET Core Blazor CSS isolation.
Partial views in ASP.NET Core
Article • 05/17/2023

By Steve Smith , Maher JENDOUBI , Rick Anderson , and Scott Sauber

A partial view is a Razor markup file ( .cshtml ) without a @page directive that renders
HTML output within another markup file's rendered output.

The term partial view is used when developing either an MVC app, where markup files
are called views, or a Razor Pages app, where markup files are called pages. This topic
generically refers to MVC views and Razor Pages pages as markup files.

View or download sample code (how to download)

When to use partial views


Partial views are an effective way to:

Break up large markup files into smaller components.

In a large, complex markup file composed of several logical pieces, there's an


advantage to working with each piece isolated into a partial view. The code in the
markup file is manageable because the markup only contains the overall page
structure and references to partial views.

Reduce the duplication of common markup content across markup files.

When the same markup elements are used across markup files, a partial view
removes the duplication of markup content into one partial view file. When the
markup is changed in the partial view, it updates the rendered output of the
markup files that use the partial view.

Partial views shouldn't be used to maintain common layout elements. Common layout
elements should be specified in _Layout.cshtml files.

Don't use a partial view where complex rendering logic or code execution is required to
render the markup. Instead of a partial view, use a view component.

Declare partial views


A partial view is a .cshtml markup file without an @page directive maintained within
the Views folder (MVC) or Pages folder (Razor Pages).
In ASP.NET Core MVC, a controller's ViewResult is capable of returning either a view or a
partial view. In Razor Pages, a PageModel can return a partial view represented as a
PartialViewResult object. Referencing and rendering partial views is described in the
Reference a partial view section.

Unlike MVC view or page rendering, a partial view doesn't run _ViewStart.cshtml . For
more information on _ViewStart.cshtml , see Layout in ASP.NET Core.

Partial view file names often begin with an underscore ( _ ). This naming convention isn't
required, but it helps to visually differentiate partial views from views and pages.

Reference a partial view

Use a partial view in a Razor Pages PageModel


In ASP.NET Core 2.0 or 2.1, the following handler method renders the
_AuthorPartialRP.cshtml partial view to the response:

C#

public IActionResult OnGetPartial() =>


new PartialViewResult
{
ViewName = "_AuthorPartialRP",
ViewData = ViewData,
};

In ASP.NET Core 2.2 or later, a handler method can alternatively call the Partial method
to produce a PartialViewResult object:

C#

public IActionResult OnGetPartial() =>


Partial("_AuthorPartialRP");

Use a partial view in a markup file


Within a markup file, there are several ways to reference a partial view. We recommend
that apps use one of the following asynchronous rendering approaches:

Partial Tag Helper


Asynchronous HTML Helper
Partial Tag Helper
The Partial Tag Helper requires ASP.NET Core 2.1 or later.

The Partial Tag Helper renders content asynchronously and uses an HTML-like syntax:

CSHTML

<partial name="_PartialName" />

When a file extension is present, the Tag Helper references a partial view that must be in
the same folder as the markup file calling the partial view:

CSHTML

<partial name="_PartialName.cshtml" />

The following example references a partial view from the app root. Paths that start with
a tilde-slash ( ~/ ) or a slash ( / ) refer to the app root:

Razor Pages

CSHTML

<partial name="~/Pages/Folder/_PartialName.cshtml" />


<partial name="/Pages/Folder/_PartialName.cshtml" />

MVC

CSHTML

<partial name="~/Views/Folder/_PartialName.cshtml" />


<partial name="/Views/Folder/_PartialName.cshtml" />

The following example references a partial view with a relative path:

CSHTML

<partial name="../Account/_PartialName.cshtml" />

For more information, see Partial Tag Helper in ASP.NET Core.

Asynchronous HTML Helper


When using an HTML Helper, the best practice is to use PartialAsync. PartialAsync
returns an IHtmlContent type wrapped in a Task<TResult>. The method is referenced by
prefixing the awaited call with an @ character:

CSHTML

@await Html.PartialAsync("_PartialName")

When the file extension is present, the HTML Helper references a partial view that must
be in the same folder as the markup file calling the partial view:

CSHTML

@await Html.PartialAsync("_PartialName.cshtml")

The following example references a partial view from the app root. Paths that start with
a tilde-slash ( ~/ ) or a slash ( / ) refer to the app root:

Razor Pages

CSHTML

@await Html.PartialAsync("~/Pages/Folder/_PartialName.cshtml")
@await Html.PartialAsync("/Pages/Folder/_PartialName.cshtml")

MVC

CSHTML

@await Html.PartialAsync("~/Views/Folder/_PartialName.cshtml")
@await Html.PartialAsync("/Views/Folder/_PartialName.cshtml")

The following example references a partial view with a relative path:

CSHTML

@await Html.PartialAsync("../Account/_LoginPartial.cshtml")

Alternatively, you can render a partial view with RenderPartialAsync. This method
doesn't return an IHtmlContent. It streams the rendered output directly to the response.
Because the method doesn't return a result, it must be called within a Razor code block:

CSHTML
@{
await Html.RenderPartialAsync("_AuthorPartial");
}

Since RenderPartialAsync streams rendered content, it provides better performance in


some scenarios. In performance-critical situations, benchmark the page using both
approaches and use the approach that generates a faster response.

Synchronous HTML Helper


Partial and RenderPartial are the synchronous equivalents of PartialAsync and
RenderPartialAsync , respectively. The synchronous equivalents aren't recommended

because there are scenarios in which they deadlock. The synchronous methods are
targeted for removal in a future release.

) Important

If you need to execute code, use a view component instead of a partial view.

Calling Partial or RenderPartial results in a Visual Studio analyzer warning. For


example, the presence of Partial yields the following warning message:

Use of IHtmlHelper.Partial may result in application deadlocks. Consider using


<partial> Tag Helper or IHtmlHelper.PartialAsync.

Replace calls to @Html.Partial with @await Html.PartialAsync or the Partial Tag Helper.
For more information on Partial Tag Helper migration, see Migrate from an HTML
Helper.

Partial view discovery


When a partial view is referenced by name without a file extension, the following
locations are searched in the stated order:

Razor Pages

1. Currently executing page's folder


2. Directory graph above the page's folder
3. /Shared
4. /Pages/Shared
5. /Views/Shared

MVC

1. /Areas/<Area-Name>/Views/<Controller-Name>
2. /Areas/<Area-Name>/Views/Shared
3. /Views/Shared
4. /Pages/Shared

The following conventions apply to partial view discovery:

Different partial views with the same file name are allowed when the partial views
are in different folders.
When referencing a partial view by name without a file extension and the partial
view is present in both the caller's folder and the Shared folder, the partial view in
the caller's folder supplies the partial view. If the partial view isn't present in the
caller's folder, the partial view is provided from the Shared folder. Partial views in
the Shared folder are called shared partial views or default partial views.
Partial views can be chained—a partial view can call another partial view if a
circular reference isn't formed by the calls. Relative paths are always relative to the
current file, not to the root or parent of the file.

7 Note

A Razor section defined in a partial view is invisible to parent markup files. The
section is only visible to the partial view in which it's defined.

Access data from partial views


When a partial view is instantiated, it receives a copy of the parent's ViewData dictionary.
Updates made to the data within the partial view aren't persisted to the parent view.
ViewData changes in a partial view are lost when the partial view returns.

The following example demonstrates how to pass an instance of ViewDataDictionary to


a partial view:

CSHTML

@await Html.PartialAsync("_PartialName", customViewData)


You can pass a model into a partial view. The model can be a custom object. You can
pass a model with PartialAsync (renders a block of content to the caller) or
RenderPartialAsync (streams the content to the output):

CSHTML

@await Html.PartialAsync("_PartialName", model)

Razor Pages

The following markup in the sample app is from the Pages/ArticlesRP/ReadRP.cshtml


page. The page contains two partial views. The second partial view passes in a model
and ViewData to the partial view. The ViewDataDictionary constructor overload is used
to pass a new ViewData dictionary while retaining the existing ViewData dictionary.

CSHTML

@model ReadRPModel

<h2>@Model.Article.Title</h2>
@* Pass the author's name to Pages\Shared\_AuthorPartialRP.cshtml *@
@await Html.PartialAsync("../Shared/_AuthorPartialRP",
Model.Article.AuthorName)
@Model.Article.PublicationDate

@* Loop over the Sections and pass in a section and additional ViewData to
the strongly typed Pages\ArticlesRP\_ArticleSectionRP.cshtml partial
view. *@
@{
var index = 0;

foreach (var section in Model.Article.Sections)


{
await Html.PartialAsync("_ArticleSectionRP",
section,
new ViewDataDictionary(ViewData)
{
{ "index", index }
});

index++;
}
}

Pages/Shared/_AuthorPartialRP.cshtml is the first partial view referenced by the

ReadRP.cshtml markup file:

CSHTML
@model string
<div>
<h3>@Model</h3>
This partial view from /Pages/Shared/_AuthorPartialRP.cshtml.
</div>

Pages/ArticlesRP/_ArticleSectionRP.cshtml is the second partial view referenced by the

ReadRP.cshtml markup file:

CSHTML

@using PartialViewsSample.ViewModels
@model ArticleSection

<h3>@Model.Title Index: @ViewData["index"]</h3>


<div>
@Model.Content
</div>

MVC

The following markup in the sample app shows the Views/Articles/Read.cshtml view.
The view contains two partial views. The second partial view passes in a model and
ViewData to the partial view. The ViewDataDictionary constructor overload is used to

pass a new ViewData dictionary while retaining the existing ViewData dictionary.

CSHTML

@model PartialViewsSample.ViewModels.Article

<h2>@Model.Title</h2>
@* Pass the author's name to Views\Shared\_AuthorPartial.cshtml *@
@await Html.PartialAsync("_AuthorPartial", Model.AuthorName)
@Model.PublicationDate

@* Loop over the Sections and pass in a section and additional ViewData to
the strongly typed Views\Articles\_ArticleSection.cshtml partial view. *@
@{
var index = 0;

foreach (var section in Model.Sections)


{
@(await Html.PartialAsync("_ArticleSection",
section,
new ViewDataDictionary(ViewData)
{
{ "index", index }
}))
index++;
}
}

Views/Shared/_AuthorPartial.cshtml is the first partial view referenced by the

Read.cshtml markup file:

CSHTML

@model string
<div>
<h3>@Model</h3>
This partial view from /Views/Shared/_AuthorPartial.cshtml.
</div>

Views/Articles/_ArticleSection.cshtml is the second partial view referenced by the

Read.cshtml markup file:

CSHTML

@using PartialViewsSample.ViewModels
@model ArticleSection

<h3>@Model.Title Index: @ViewData["index"]</h3>


<div>
@Model.Content
</div>

At runtime, the partials are rendered into the parent markup file's rendered output,
which itself is rendered within the shared _Layout.cshtml . The first partial view renders
the article author's name and publication date:

Abraham Lincoln

This partial view from <shared partial view file path>. 11/19/1863 12:00:00 AM

The second partial view renders the article's sections:

Section One Index: 0

Four score and seven years ago ...

Section Two Index: 1

Now we are engaged in a great civil war, testing ...


Section Three Index: 2

But, in a larger sense, we can not dedicate ...

Additional resources
Razor syntax reference for ASP.NET Core
Tag Helpers in ASP.NET Core
Partial Tag Helper in ASP.NET Core
View components in ASP.NET Core
Areas in ASP.NET Core
Handle requests with controllers in
ASP.NET Core MVC
Article • 04/25/2023

By Steve Smith and Scott Addie

Controllers, actions, and action results are a fundamental part of how developers build
apps using ASP.NET Core MVC.

What is a Controller?
A controller is used to define and group a set of actions. An action (or action method) is
a method on a controller which handles requests. Controllers logically group similar
actions together. This aggregation of actions allows common sets of rules, such as
routing, caching, and authorization, to be applied collectively. Requests are mapped to
actions through routing. Controllers are activated and disposed on a per request basis.

By convention, controller classes:

Reside in the project's root-level Controllers folder.


Inherit from Microsoft.AspNetCore.Mvc.Controller .

A controller is an instantiable class, usually public, in which at least one of the following
conditions is true:

The class name is suffixed with Controller .


The class inherits from a class whose name is suffixed with Controller .
The [Controller] attribute is applied to the class.

A controller class must not have an associated [NonController] attribute.

Controllers should follow the Explicit Dependencies Principle. There are a couple of
approaches to implementing this principle. If multiple controller actions require the
same service, consider using constructor injection to request those dependencies. If the
service is needed by only a single action method, consider using Action Injection to
request the dependency.

Within the Model-View-Controller pattern, a controller is responsible for the initial


processing of the request and instantiation of the model. Generally, business decisions
should be performed within the model.
The controller takes the result of the model's processing (if any) and returns either the
proper view and its associated view data or the result of the API call. Learn more at
Overview of ASP.NET Core MVC and Get started with ASP.NET Core MVC and Visual
Studio.

The controller is a UI-level abstraction. Its responsibilities are to ensure request data is
valid and to choose which view (or result for an API) should be returned. In well-factored
apps, it doesn't directly include data access or business logic. Instead, the controller
delegates to services handling these responsibilities.

Defining Actions
Public methods on a controller, except those with the [NonAction] attribute, are actions.
Parameters on actions are bound to request data and are validated using model
binding. Model validation occurs for everything that's model-bound. The
ModelState.IsValid property value indicates whether model binding and validation

succeeded.

Action methods should contain logic for mapping a request to a business concern.
Business concerns should typically be represented as services that the controller
accesses through dependency injection. Actions then map the result of the business
action to an application state.

Actions can return anything, but frequently return an instance of IActionResult (or
Task<IActionResult> for async methods) that produces a response. The action method

is responsible for choosing what kind of response. The action result does the responding.

Controller Helper Methods


Controllers usually inherit from Controller, although this isn't required. Deriving from
Controller provides access to three categories of helper methods:

1. Methods resulting in an empty response body

No Content-Type HTTP response header is included, since the response body lacks
content to describe.

There are two result types within this category: Redirect and HTTP Status Code.

HTTP Status Code


This type returns an HTTP status code. A couple of helper methods of this type are
BadRequest , NotFound , and Ok . For example, return BadRequest(); produces a 400
status code when executed. When methods such as BadRequest , NotFound , and Ok
are overloaded, they no longer qualify as HTTP Status Code responders, since
content negotiation is taking place.

Redirect

This type returns a redirect to an action or destination (using Redirect ,


LocalRedirect , RedirectToAction , or RedirectToRoute ). For example, return

RedirectToAction("Complete", new {id = 123}); redirects to Complete , passing an


anonymous object.

The Redirect result type differs from the HTTP Status Code type primarily in the
addition of a Location HTTP response header.

2. Methods resulting in a non-empty response body with a


predefined content type
Most helper methods in this category include a ContentType property, allowing you to
set the Content-Type response header to describe the response body.

There are two result types within this category: View and Formatted Response.

View

This type returns a view which uses a model to render HTML. For example, return
View(customer); passes a model to the view for data-binding.

Formatted Response

This type returns JSON or a similar data exchange format to represent an object in
a specific manner. For example, return Json(customer); serializes the provided
object into JSON format.

Other common methods of this type include File and PhysicalFile . For example,
return PhysicalFile(customerFilePath, "text/xml"); returns PhysicalFileResult.

3. Methods resulting in a non-empty response body formatted in a


content type negotiated with the client
This category is better known as Content Negotiation. Content negotiation applies
whenever an action returns an ObjectResult type or something other than an
IActionResult implementation. An action that returns a non- IActionResult
implementation (for example, object ) also returns a Formatted Response.

Some helper methods of this type include BadRequest , CreatedAtRoute , and Ok .


Examples of these methods include return BadRequest(modelState); , return
CreatedAtRoute("routename", values, newobject); , and return Ok(value); , respectively.

Note that BadRequest and Ok perform content negotiation only when passed a value;
without being passed a value, they instead serve as HTTP Status Code result types. The
CreatedAtRoute method, on the other hand, always performs content negotiation since

its overloads all require that a value be passed.

Cross-Cutting Concerns
Applications typically share parts of their workflow. Examples include an app that
requires authentication to access the shopping cart, or an app that caches data on some
pages. To perform logic before or after an action method, use a filter. Using Filters on
cross-cutting concerns can reduce duplication.

Most filter attributes, such as [Authorize] , can be applied at the controller or action
level depending upon the desired level of granularity.

Error handling and response caching are often cross-cutting concerns:

Handle errors
Response Caching

Many cross-cutting concerns can be handled using filters or custom middleware.


Routing to controller actions in ASP.NET
Core
Article • 02/02/2023

By Ryan Nowak , Kirk Larkin , and Rick Anderson

ASP.NET Core controllers use the Routing middleware to match the URLs of incoming
requests and map them to actions. Route templates:

Are defined at startup in Program.cs or in attributes.


Describe how URL paths are matched to actions.
Are used to generate URLs for links. The generated links are typically returned in
responses.

Actions are either conventionally-routed or attribute-routed. Placing a route on the


controller or action makes it attribute-routed. See Mixed routing for more information.

This document:

Explains the interactions between MVC and routing:


How typical MVC apps make use of routing features.
Covers both:
Conventional routing typically used with controllers and views.
Attribute routing used with REST APIs. If you're primarily interested in routing
for REST APIs, jump to the Attribute routing for REST APIs section.
See Routing for advanced routing details.
Refers to the default routing system called endpoint routing. It's possible to use
controllers with the previous version of routing for compatibility purposes. See the
2.2-3.0 migration guide for instructions.

Set up conventional route


The ASP.NET Core MVC template generates conventional routing code similar to the
following:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();


if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

MapControllerRoute is used to create a single route. The single route is named default
route. Most apps with controllers and views use a route template similar to the default
route. REST APIs should use attribute routing.

C#

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

The route template "{controller=Home}/{action=Index}/{id?}" :

Matches a URL path like /Products/Details/5

Extracts the route values { controller = Products, action = Details, id = 5 } by


tokenizing the path. The extraction of route values results in a match if the app has
a controller named ProductsController and a Details action:

C#

public class ProductsController : Controller


{
public IActionResult Details(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
MyDisplayRouteInfo is provided by the Rick.Docs.Samples.RouteInfo NuGet
package and displays route information.

/Products/Details/5 model binds the value of id = 5 to set the id parameter to

5 . See Model Binding for more details.

{controller=Home} defines Home as the default controller .

{action=Index} defines Index as the default action .

The ? character in {id?} defines id as optional.


Default and optional route parameters don't need to be present in the URL path
for a match. See Route Template Reference for a detailed description of route
template syntax.

Matches the URL path / .

Produces the route values { controller = Home, action = Index } .

The values for controller and action make use of the default values. id doesn't
produce a value since there's no corresponding segment in the URL path. / only
matches if there exists a HomeController and Index action:

C#

public class HomeController : Controller


{
public IActionResult Index() { ... }
}

Using the preceding controller definition and route template, the HomeController.Index
action is run for the following URL paths:

/Home/Index/17

/Home/Index
/Home

The URL path / uses the route template default Home controllers and Index action. The
URL path /Home uses the route template default Index action.

The convenience method MapDefaultControllerRoute:

C#
app.MapDefaultControllerRoute();

Replaces:

C#

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

) Important

Routing is configured using the UseRouting and UseEndpoints middleware. To use


controllers:

Call MapControllers to map attribute routed controllers.


Call MapControllerRoute or MapAreaControllerRoute, to map both
conventionally routed controllers and attribute routed controllers.

Apps typically don't need to call UseRouting or UseEndpoints .


WebApplicationBuilder configures a middleware pipeline that wraps middleware
added in Program.cs with UseRouting and UseEndpoints . For more information, see
Routing in ASP.NET Core.

Conventional routing
Conventional routing is used with controllers and views. The default route:

C#

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

The preceding is an example of a conventional route. It's called conventional routing


because it establishes a convention for URL paths:

The first path segment, {controller=Home} , maps to the controller name.


The second segment, {action=Index} , maps to the action name.
The third segment, {id?} is used for an optional id . The ? in {id?} makes it
optional. id is used to map to a model entity.

Using this default route, the URL path:

/Products/List maps to the ProductsController.List action.


/Blog/Article/17 maps to BlogController.Article and typically model binds the

id parameter to 17.

This mapping:

Is based on the controller and action names only.


Isn't based on namespaces, source file locations, or method parameters.

Using conventional routing with the default route allows creating the app without
having to come up with a new URL pattern for each action. For an app with CRUD
style actions, having consistency for the URLs across controllers:

Helps simplify the code.


Makes the UI more predictable.

2 Warning

The id in the preceding code is defined as optional by the route template. Actions
can execute without the optional ID provided as part of the URL. Generally, when
id is omitted from the URL:

id is set to 0 by model binding.

No entity is found in the database matching id == 0 .

Attribute routing provides fine-grained control to make the ID required for some
actions and not for others. By convention, the documentation includes optional
parameters like id when they're likely to appear in correct usage.

Most apps should choose a basic and descriptive routing scheme so that URLs are
readable and meaningful. The default conventional route
{controller=Home}/{action=Index}/{id?} :

Supports a basic and descriptive routing scheme.


Is a useful starting point for UI-based apps.
Is the only route template needed for many web UI apps. For larger web UI apps,
another route using Areas is frequently all that's needed.
MapControllerRoute and MapAreaRoute :

Automatically assign an order value to their endpoints based on the order they are
invoked.

Endpoint routing in ASP.NET Core:

Doesn't have a concept of routes.


Doesn't provide ordering guarantees for the execution of extensibility, all
endpoints are processed at once.

Enable Logging to see how the built-in routing implementations, such as Route, match
requests.

Attribute routing is explained later in this document.

Multiple conventional routes


Multiple conventional routes can be configured by adding more calls to
MapControllerRoute and MapAreaControllerRoute. Doing so allows defining multiple
conventions, or to adding conventional routes that are dedicated to a specific action,
such as:

C#

app.MapControllerRoute(name: "blog",
pattern: "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

The blog route in the preceding code is a dedicated conventional route. It's called a
dedicated conventional route because:

It uses conventional routing.


It's dedicated to a specific action.

Because controller and action don't appear in the route template "blog/{*article}"
as parameters:

They can only have the default values { controller = "Blog", action = "Article"
}.

This route always maps to the action BlogController.Article .


/Blog , /Blog/Article , and /Blog/{any-string} are the only URL paths that match the

blog route.

The preceding example:

blog route has a higher priority for matches than the default route because it is
added first.
Is an example of Slug style routing where it's typical to have an article name as
part of the URL.

2 Warning

In ASP.NET Core, routing doesn't:

Define a concept called a route. UseRouting adds route matching to the


middleware pipeline. The UseRouting middleware looks at the set of
endpoints defined in the app, and selects the best endpoint match based on
the request.
Provide guarantees about the execution order of extensibility like
IRouteConstraint or IActionConstraint.

See Routing for reference material on routing.

Conventional routing order


Conventional routing only matches a combination of action and controller that are
defined by the app. This is intended to simplify cases where conventional routes overlap.
Adding routes using MapControllerRoute, MapDefaultControllerRoute, and
MapAreaControllerRoute automatically assign an order value to their endpoints based
on the order they are invoked. Matches from a route that appears earlier have a higher
priority. Conventional routing is order-dependent. In general, routes with areas should
be placed earlier as they're more specific than routes without an area. Dedicated
conventional routes with catch-all route parameters like {*article} can make a route
too greedy, meaning that it matches URLs that you intended to be matched by other
routes. Put the greedy routes later in the route table to prevent greedy matches.

Resolving ambiguous actions


When two endpoints match through routing, routing must do one of the following:

Choose the best candidate.


Throw an exception.

For example:

C#

public class Products33Controller : Controller


{
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[HttpPost]
public IActionResult Edit(int id, Product product)
{
return ControllerContext.MyDisplayRouteInfo(id, product.name);
}
}

The preceding controller defines two actions that match:

The URL path /Products33/Edit/17


Route data { controller = Products33, action = Edit, id = 17 } .

This is a typical pattern for MVC controllers:

Edit(int) displays a form to edit a product.

Edit(int, Product) processes the posted form.

To resolve the correct route:

Edit(int, Product) is selected when the request is an HTTP POST .

Edit(int) is selected when the HTTP verb is anything else. Edit(int) is generally
called via GET .

The HttpPostAttribute, [HttpPost] , is provided to routing so that it can choose based on


the HTTP method of the request. The HttpPostAttribute makes Edit(int, Product) a
better match than Edit(int) .

It's important to understand the role of attributes like HttpPostAttribute . Similar


attributes are defined for other HTTP verbs. In conventional routing, it's common for
actions to use the same action name when they're part of a show form, submit form
workflow. For example, see Examine the two Edit action methods.
If routing can't choose a best candidate, an AmbiguousMatchException is thrown, listing
the multiple matched endpoints.

Conventional route names


The strings "blog" and "default" in the following examples are conventional route
names:

C#

app.MapControllerRoute(name: "blog",
pattern: "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

The route names give the route a logical name. The named route can be used for URL
generation. Using a named route simplifies URL creation when the ordering of routes
could make URL generation complicated. Route names must be unique application wide.

Route names:

Have no impact on URL matching or handling of requests.


Are used only for URL generation.

The route name concept is represented in routing as IEndpointNameMetadata. The


terms route name and endpoint name:

Are interchangeable.
Which one is used in documentation and code depends on the API being
described.

Attribute routing for REST APIs


REST APIs should use attribute routing to model the app's functionality as a set of
resources where operations are represented by HTTP verbs.

Attribute routing uses a set of attributes to map actions directly to route templates. The
following code is typical for a REST API and is used in the next sample:

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

In the preceding code, MapControllers is called to map attribute routed controllers.

In the following example:

HomeController matches a set of URLs similar to what the default conventional

route {controller=Home}/{action=Index}/{id?} matches.

C#

public class HomeController : Controller


{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
[Route("Home/Index/{id?}")]
public IActionResult Index(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[Route("Home/About")]
[Route("Home/About/{id?}")]
public IActionResult About(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

The HomeController.Index action is run for any of the URL paths / , /Home , /Home/Index ,
or /Home/Index/3 .

This example highlights a key programming difference between attribute routing and
conventional routing. Attribute routing requires more input to specify a route. The
conventional default route handles routes more succinctly. However, attribute routing
allows and requires precise control of which route templates apply to each action.
With attribute routing, the controller and action names play no part in which action is
matched, unless token replacement is used. The following example matches the same
URLs as the previous example:

C#

public class MyDemoController : Controller


{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
[Route("Home/Index/{id?}")]
public IActionResult MyIndex(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[Route("Home/About")]
[Route("Home/About/{id?}")]
public IActionResult MyAbout(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

The following code uses token replacement for action and controller :

C#

public class HomeController : Controller


{
[Route("")]
[Route("Home")]
[Route("[controller]/[action]")]
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}

[Route("[controller]/[action]")]
public IActionResult About()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

The following code applies [Route("[controller]/[action]")] to the controller:

C#
[Route("[controller]/[action]")]
public class HomeController : Controller
{
[Route("~/")]
[Route("/Home")]
[Route("~/Home/Index")]
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}

public IActionResult About()


{
return ControllerContext.MyDisplayRouteInfo();
}
}

In the preceding code, the Index method templates must prepend / or ~/ to the route
templates. Route templates applied to an action that begin with / or ~/ don't get
combined with route templates applied to the controller.

See Route template precedence for information on route template selection.

Reserved routing names


The following keywords are reserved route parameter names when using Controllers or
Razor Pages:

action
area

controller

handler
page

Using page as a route parameter with attribute routing is a common error. Doing that
results in inconsistent and confusing behavior with URL generation.

C#

public class MyDemo2Controller : Controller


{
[Route("/articles/{page}")]
public IActionResult ListArticles(int page)
{
return ControllerContext.MyDisplayRouteInfo(page);
}
}

The special parameter names are used by the URL generation to determine if a URL
generation operation refers to a Razor Page or to a Controller.

The following keywords are reserved in the context of a Razor view or a Razor Page:

page
using

namespace

inject
section

inherits
model

addTagHelper

removeTagHelper

These keywords shouldn't be used for link generations, model bound parameters, or top
level properties.

HTTP verb templates


ASP.NET Core has the following HTTP verb templates:

[HttpGet]
[HttpPost]
[HttpPut]
[HttpDelete]
[HttpHead]
[HttpPatch]

Route templates
ASP.NET Core has the following route templates:

All the HTTP verb templates are route templates.


[Route]

Attribute routing with Http verb attributes


Consider the following controller:

C#

[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
[HttpGet] // GET /api/test2
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("{id}")] // GET /api/test2/xyz


public IActionResult GetProduct(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[HttpGet("int/{id:int}")] // GET /api/test2/int/3


public IActionResult GetIntProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[HttpGet("int2/{id}")] // GET /api/test2/int2/3


public IActionResult GetInt2Product(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

In the preceding code:

Each action contains the [HttpGet] attribute, which constrains matching to HTTP
GET requests only.
The GetProduct action includes the "{id}" template, therefore id is appended to
the "api/[controller]" template on the controller. The methods template is
"api/[controller]/{id}" . Therefore this action only matches GET requests for the

form /api/test2/xyz , /api/test2/123 , /api/test2/{any string} , etc.

C#

[HttpGet("{id}")] // GET /api/test2/xyz


public IActionResult GetProduct(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
The GetIntProduct action contains the "int/{id:int}" template. The :int portion
of the template constrains the id route values to strings that can be converted to
an integer. A GET request to /api/test2/int/abc :
Doesn't match this action.
Returns a 404 Not Found error.

C#

[HttpGet("int/{id:int}")] // GET /api/test2/int/3


public IActionResult GetIntProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

The GetInt2Product action contains {id} in the template, but doesn't constrain id
to values that can be converted to an integer. A GET request to
/api/test2/int2/abc :

Matches this route.


Model binding fails to convert abc to an integer. The id parameter of the
method is integer.
Returns a 400 Bad Request because model binding failed to convert abc to
an integer.

C#

[HttpGet("int2/{id}")] // GET /api/test2/int2/3


public IActionResult GetInt2Product(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

Attribute routing can use HttpMethodAttribute attributes such as HttpPostAttribute,


HttpPutAttribute, and HttpDeleteAttribute. All of the HTTP verb attributes accept a route
template. The following example shows two actions that match the same route
template:

C#

[ApiController]
public class MyProductsController : ControllerBase
{
[HttpGet("/products3")]
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}
[HttpPost("/products3")]
public IActionResult CreateProduct(MyProduct myProduct)
{
return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
}
}

Using the URL path /products3 :

The MyProductsController.ListProducts action runs when the HTTP verb is GET .


The MyProductsController.CreateProduct action runs when the HTTP verb is POST .

When building a REST API, it's rare that you'll need to use [Route(...)] on an action
method because the action accepts all HTTP methods. It's better to use the more
specific HTTP verb attribute to be precise about what your API supports. Clients of REST
APIs are expected to know what paths and HTTP verbs map to specific logical
operations.

REST APIs should use attribute routing to model the app's functionality as a set of
resources where operations are represented by HTTP verbs. This means that many
operations, for example, GET and POST on the same logical resource use the same URL.
Attribute routing provides a level of control that's needed to carefully design an API's
public endpoint layout.

Since an attribute route applies to a specific action, it's easy to make parameters
required as part of the route template definition. In the following example, id is
required as part of the URL path:

C#

[ApiController]
public class Products2ApiController : ControllerBase
{
[HttpGet("/products2/{id}", Name = "Products_List")]
public IActionResult GetProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

The Products2ApiController.GetProduct(int) action:

Is run with URL path like /products2/3


Isn't run with the URL path /products2 .
The [Consumes] attribute allows an action to limit the supported request content types.
For more information, see Define supported request content types with the Consumes
attribute.

See Routing for a full description of route templates and related options.

For more information on [ApiController] , see ApiController attribute.

Route name
The following code defines a route name of Products_List :

C#

[ApiController]
public class Products2ApiController : ControllerBase
{
[HttpGet("/products2/{id}", Name = "Products_List")]
public IActionResult GetProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

Route names can be used to generate a URL based on a specific route. Route names:

Have no impact on the URL matching behavior of routing.


Are only used for URL generation.

Route names must be unique application-wide.

Contrast the preceding code with the conventional default route, which defines the id
parameter as optional ( {id?} ). The ability to precisely specify APIs has advantages, such
as allowing /products and /products/5 to be dispatched to different actions.

Combining attribute routes


To make attribute routing less repetitive, route attributes on the controller are combined
with route attributes on the individual actions. Any route templates defined on the
controller are prepended to route templates on the actions. Placing a route attribute on
the controller makes all actions in the controller use attribute routing.

C#
[ApiController]
[Route("products")]
public class ProductsApiController : ControllerBase
{
[HttpGet]
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

In the preceding example:

The URL path /products can match ProductsApi.ListProducts


The URL path /products/5 can match ProductsApi.GetProduct(int) .

Both of these actions only match HTTP GET because they're marked with the [HttpGet]
attribute.

Route templates applied to an action that begin with / or ~/ don't get combined with
route templates applied to the controller. The following example matches a set of URL
paths similar to the default route.

C#

[Route("Home")]
public class HomeController : Controller
{
[Route("")]
[Route("Index")]
[Route("/")]
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}

[Route("About")]
public IActionResult About()
{
return ControllerContext.MyDisplayRouteInfo();
}
}
The following table explains the [Route] attributes in the preceding code:

Attribute Combines with [Route("Home")] Defines route template

[Route("")] Yes "Home"

[Route("Index")] Yes "Home/Index"

[Route("/")] No ""

[Route("About")] Yes "Home/About"

Attribute route order


Routing builds a tree and matches all endpoints simultaneously:

The route entries behave as if placed in an ideal ordering.


The most specific routes have a chance to execute before the more general routes.

For example, an attribute route like blog/search/{topic} is more specific than an


attribute route like blog/{*article} . The blog/search/{topic} route has higher priority,
by default, because it's more specific. Using conventional routing, the developer is
responsible for placing routes in the desired order.

Attribute routes can configure an order using the Order property. All of the framework
provided route attributes include Order . Routes are processed according to an
ascending sort of the Order property. The default order is 0 . Setting a route using Order
= -1 runs before routes that don't set an order. Setting a route using Order = 1 runs

after default route ordering.

Avoid depending on Order . If an app's URL-space requires explicit order values to route
correctly, then it's likely confusing to clients as well. In general, attribute routing selects
the correct route with URL matching. If the default order used for URL generation isn't
working, using a route name as an override is usually simpler than applying the Order
property.

Consider the following two controllers which both define the route matching /home :

C#

public class HomeController : Controller


{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
[Route("Home/Index/{id?}")]
public IActionResult Index(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[Route("Home/About")]
[Route("Home/About/{id?}")]
public IActionResult About(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

C#

public class MyDemoController : Controller


{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
[Route("Home/Index/{id?}")]
public IActionResult MyIndex(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[Route("Home/About")]
[Route("Home/About/{id?}")]
public IActionResult MyAbout(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

Requesting /home with the preceding code throws an exception similar to the following:

text

AmbiguousMatchException: The request matched multiple endpoints. Matches:

WebMvcRouting.Controllers.HomeController.Index
WebMvcRouting.Controllers.MyDemoController.MyIndex

Adding Order to one of the route attributes resolves the ambiguity:

C#

[Route("")]
[Route("Home", Order = 2)]
[Route("Home/MyIndex")]
public IActionResult MyIndex()
{
return ControllerContext.MyDisplayRouteInfo();
}

With the preceding code, /home runs the HomeController.Index endpoint. To get to the
MyDemoController.MyIndex , request /home/MyIndex . Note:

The preceding code is an example or poor routing design. It was used to illustrate
the Order property.
The Order property only resolves the ambiguity, that template cannot be matched.
It would be better to remove the [Route("Home")] template.

See Razor Pages route and app conventions: Route order for information on route order
with Razor Pages.

In some cases, an HTTP 500 error is returned with ambiguous routes. Use logging to see
which endpoints caused the AmbiguousMatchException .

Token replacement in route templates


[controller], [action], [area]
For convenience, attribute routes support token replacement by enclosing a token in
square-brackets ( [ , ] ). The tokens [action] , [area] , and [controller] are replaced
with the values of the action name, area name, and controller name from the action
where the route is defined:

C#

[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
[HttpGet]
public IActionResult List()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("{id}")]
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
In the preceding code:

C#

[HttpGet]
public IActionResult List()
{
return ControllerContext.MyDisplayRouteInfo();
}

Matches /Products0/List

C#

[HttpGet("{id}")]
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

Matches /Products0/Edit/{id}

Token replacement occurs as the last step of building the attribute routes. The
preceding example behaves the same as the following code:

C#

public class Products20Controller : Controller


{
[HttpGet("[controller]/[action]")] // Matches '/Products20/List'
public IActionResult List()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("[controller]/[action]/{id}")] // Matches
'/Products20/Edit/{id}'
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

If you are reading this in a language other than English, let us know in this GitHub
discussion issue if you'd like to see the code comments in your native language.
Attribute routes can also be combined with inheritance. This is powerful combined with
token replacement. Token replacement also applies to route names defined by attribute
routes. [Route("[controller]/[action]", Name="[controller]_[action]")] generates a
unique route name for each action:

C#

[ApiController]
[Route("api/[controller]/[action]", Name = "[controller]_[action]")]
public abstract class MyBase2Controller : ControllerBase
{
}

public class Products11Controller : MyBase2Controller


{
[HttpGet] // /api/products11/list
public IActionResult List()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("{id}")] // /api/products11/edit/3
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

To match the literal token replacement delimiter [ or ] , escape it by repeating the


character ( [[ or ]] ).

Use a parameter transformer to customize token


replacement
Token replacement can be customized using a parameter transformer. A parameter
transformer implements IOutboundParameterTransformer and transforms the value of
parameters. For example, a custom SlugifyParameterTransformer parameter transformer
changes the SubscriptionManagement route value to subscription-management :

C#

using System.Text.RegularExpressions;

public class SlugifyParameterTransformer : IOutboundParameterTransformer


{
public string? TransformOutbound(object? value)
{
if (value == null) { return null; }

return Regex.Replace(value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,

TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}

The RouteTokenTransformerConvention is an application model convention that:

Applies a parameter transformer to all attribute routes in an application.


Customizes the attribute route token values as they are replaced.

C#

public class SubscriptionManagementController : Controller


{
[HttpGet("[controller]/[action]")]
public IActionResult ListAll()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

The preceding ListAll method matches /subscription-management/list-all .

The RouteTokenTransformerConvention is registered as an option:

C#

using Microsoft.AspNetCore.Mvc.ApplicationModels;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

See MDN web docs on Slug for the definition of Slug.

2 Warning

When using System.Text.RegularExpressions to process untrusted input, pass a


timeout. A malicious user can provide input to RegularExpressions causing a
Denial-of-Service attack . ASP.NET Core framework APIs that use
RegularExpressions pass a timeout.

Multiple attribute routes


Attribute routing supports defining multiple routes that reach the same action. The most
common usage of this is to mimic the behavior of the default conventional route as
shown in the following example:

C#

[Route("[controller]")]
public class Products13Controller : Controller
{
[Route("")] // Matches 'Products13'
[Route("Index")] // Matches 'Products13/Index'
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}

Putting multiple route attributes on the controller means that each one combines with
each of the route attributes on the action methods:

C#

[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
[HttpPost("Buy")] // Matches 'Products6/Buy' and 'Store/Buy'
[HttpPost("Checkout")] // Matches 'Products6/Checkout' and
'Store/Checkout'
public IActionResult Buy()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

All the HTTP verb route constraints implement IActionConstraint .

When multiple route attributes that implement IActionConstraint are placed on an


action:

Each action constraint combines with the route template applied to the controller.

C#

[Route("api/[controller]")]
public class Products7Controller : ControllerBase
{
[HttpPut("Buy")] // Matches PUT 'api/Products7/Buy'
[HttpPost("Checkout")] // Matches POST 'api/Products7/Checkout'
public IActionResult Buy()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

Using multiple routes on actions might seem useful and powerful, it's better to keep
your app's URL space basic and well defined. Use multiple routes on actions only where
needed, for example, to support existing clients.

Specifying attribute route optional parameters, default


values, and constraints
Attribute routes support the same inline syntax as conventional routes to specify
optional parameters, default values, and constraints.

C#

public class Products14Controller : Controller


{
[HttpPost("product14/{id:int}")]
public IActionResult ShowProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

In the preceding code, [HttpPost("product14/{id:int}")] applies a route constraint.


The Products14Controller.ShowProduct action is matched only by URL paths like
/product14/3 . The route template portion {id:int} constrains that segment to only

integers.

See Route Template Reference for a detailed description of route template syntax.

Custom route attributes using IRouteTemplateProvider


All of the route attributes implement IRouteTemplateProvider. The ASP.NET Core
runtime:

Looks for attributes on controller classes and action methods when the app starts.
Uses the attributes that implement IRouteTemplateProvider to build the initial set
of routes.

Implement IRouteTemplateProvider to define custom route attributes. Each


IRouteTemplateProvider allows you to define a single route with a custom route

template, order, and name:

C#

public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider


{
public string Template => "api/[controller]";
public int? Order => 2;
public string Name { get; set; } = string.Empty;
}

[MyApiController]
[ApiController]
public class MyTestApiController : ControllerBase
{
// GET /api/MyTestApi
[HttpGet]
public IActionResult Get()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

The preceding Get method returns Order = 2, Template = api/MyTestApi .


Use application model to customize attribute routes
The application model:

Is an object model created at startup in Program.cs .


Contains all of the metadata used by ASP.NET Core to route and execute the
actions in an app.

The application model includes all of the data gathered from route attributes. The data
from route attributes is provided by the IRouteTemplateProvider implementation.
Conventions:

Can be written to modify the application model to customize how routing behaves.
Are read at app startup.

This section shows a basic example of customizing routing using application model. The
following code makes routes roughly line up with the folder structure of the project.

C#

public class NamespaceRoutingConvention : Attribute,


IControllerModelConvention
{
private readonly string _baseNamespace;

public NamespaceRoutingConvention(string baseNamespace)


{
_baseNamespace = baseNamespace;
}

public void Apply(ControllerModel controller)


{
var hasRouteAttributes = controller.Selectors.Any(selector =>
selector.AttributeRouteModel
!= null);
if (hasRouteAttributes)
{
return;
}

var namespc = controller.ControllerType.Namespace;


if (namespc == null)
return;
var template = new StringBuilder();
template.Append(namespc, _baseNamespace.Length + 1,
namespc.Length - _baseNamespace.Length - 1);
template.Replace('.', '/');
template.Append("/[controller]/[action]/{id?}");

foreach (var selector in controller.Selectors)


{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template.ToString()
};
}
}
}

The following code prevents the namespace convention from being applied to
controllers that are attribute routed:

C#

public void Apply(ControllerModel controller)


{
var hasRouteAttributes = controller.Selectors.Any(selector =>
selector.AttributeRouteModel !=
null);
if (hasRouteAttributes)
{
return;
}

For example, the following controller doesn't use NamespaceRoutingConvention :

C#

[Route("[controller]/[action]/{id?}")]
public class ManagersController : Controller
{
// /managers/index
public IActionResult Index()
{
var template =
ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
return Content($"Index- template:{template}");
}

public IActionResult List(int? id)


{
var path = Request.Path.Value;
return Content($"List- Path:{path}");
}
}

The NamespaceRoutingConvention.Apply method:

Does nothing if the controller is attribute routed.


Sets the controllers template based on the namespace , with the base namespace
removed.

The NamespaceRoutingConvention can be applied in Program.cs :

C#

using My.Application.Controllers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(
new NamespaceRoutingConvention(typeof(HomeController).Namespace!));
});

var app = builder.Build();

For example, consider the following controller:

C#

using Microsoft.AspNetCore.Mvc;

namespace My.Application.Admin.Controllers
{
public class UsersController : Controller
{
// GET /admin/controllers/users/index
public IActionResult Index()
{
var fullname = typeof(UsersController).FullName;
var template =

ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
var path = Request.Path.Value;

return Content($"Path: {path} fullname: {fullname} template:


{template}");
}

public IActionResult List(int? id)


{
var path = Request.Path.Value;
return Content($"Path: {path} ID:{id}");
}
}
}

In the preceding code:


The base namespace is My.Application .
The full name of the preceding controller is
My.Application.Admin.Controllers.UsersController .

The NamespaceRoutingConvention sets the controllers template to


Admin/Controllers/Users/[action]/{id? .

The NamespaceRoutingConvention can also be applied as an attribute on a controller:

C#

[NamespaceRoutingConvention("My.Application")]
public class TestController : Controller
{
// /admin/controllers/test/index
public IActionResult Index()
{
var template =
ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
var actionname = ControllerContext.ActionDescriptor.ActionName;
return Content($"Action- {actionname} template:{template}");
}

public IActionResult List(int? id)


{
var path = Request.Path.Value;
return Content($"List- Path:{path}");
}
}

Mixed routing: Attribute routing vs


conventional routing
ASP.NET Core apps can mix the use of conventional routing and attribute routing. It's
typical to use conventional routes for controllers serving HTML pages for browsers, and
attribute routing for controllers serving REST APIs.

Actions are either conventionally routed or attribute routed. Placing a route on the
controller or the action makes it attribute routed. Actions that define attribute routes
cannot be reached through the conventional routes and vice-versa. Any route attribute
on the controller makes all actions in the controller attribute routed.

Attribute routing and conventional routing use the same routing engine.

Routing with special characters


Routing with special characters can lead to unexpected results. For example, consider a
controller with the following action method:

C#

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null || todoItem.Name == null)


{
return NotFound();
}

return todoItem.Name;
}

When string id contains the following encoded values, unexpected results might
occur:

ASCII Encoded

/ %2F

Route parameters are not always URL decoded. This problem may be addressed in the
future. For more information, see this GitHub issue ;

URL Generation and ambient values


Apps can use routing URL generation features to generate URL links to actions.
Generating URLs eliminates hard-coding URLs, making code more robust and
maintainable. This section focuses on the URL generation features provided by MVC and
only cover basics of how URL generation works. See Routing for a detailed description
of URL generation.

The IUrlHelper interface is the underlying element of infrastructure between MVC and
routing for URL generation. An instance of IUrlHelper is available through the Url
property in controllers, views, and view components.

In the following example, the IUrlHelper interface is used through the Controller.Url
property to generate a URL to another action.
C#

public class UrlGenerationController : Controller


{
public IActionResult Source()
{
// Generates /UrlGeneration/Destination
var url = Url.Action("Destination");
return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
}

public IActionResult Destination()


{
return ControllerContext.MyDisplayRouteInfo();
}
}

If the app is using the default conventional route, the value of the url variable is the
URL path string /UrlGeneration/Destination . This URL path is created by routing by
combining:

The route values from the current request, which are called ambient values.
The values passed to Url.Action and substituting those values into the route
template:

text

ambient values: { controller = "UrlGeneration", action = "Source" }


values passed to Url.Action: { controller = "UrlGeneration", action =
"Destination" }
route template: {controller}/{action}/{id?}

result: /UrlGeneration/Destination

Each route parameter in the route template has its value substituted by matching names
with the values and ambient values. A route parameter that doesn't have a value can:

Use a default value if it has one.


Be skipped if it's optional. For example, the id from the route template
{controller}/{action}/{id?} .

URL generation fails if any required route parameter doesn't have a corresponding
value. If URL generation fails for a route, the next route is tried until all routes have been
tried or a match is found.

The preceding example of Url.Action assumes conventional routing. URL generation


works similarly with attribute routing, though the concepts are different. With
conventional routing:

The route values are used to expand a template.


The route values for controller and action usually appear in that template. This
works because the URLs matched by routing adhere to a convention.

The following example uses attribute routing:

C#

public class UrlGenerationAttrController : Controller


{
[HttpGet("custom")]
public IActionResult Source()
{
var url = Url.Action("Destination");
return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
}

[HttpGet("custom/url/to/destination")]
public IActionResult Destination()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

The Source action in the preceding code generates custom/url/to/destination .

LinkGenerator was added in ASP.NET Core 3.0 as an alternative to IUrlHelper .


LinkGenerator offers similar but more flexible functionality. Each method on IUrlHelper

has a corresponding family of methods on LinkGenerator as well.

Generating URLs by action name


Url.Action, LinkGenerator.GetPathByAction, and all related overloads all are designed to
generate the target endpoint by specifying a controller name and action name.

When using Url.Action , the current route values for controller and action are
provided by the runtime:

The value of controller and action are part of both ambient values and values.
The method Url.Action always uses the current values of action and controller
and generates a URL path that routes to the current action.

Routing attempts to use the values in ambient values to fill in information that wasn't
provided when generating a URL. Consider a route like {a}/{b}/{c}/{d} with ambient
values { a = Alice, b = Bob, c = Carol, d = David } :

Routing has enough information to generate a URL without any additional values.
Routing has enough information because all route parameters have a value.

If the value { d = Donovan } is added:

The value { d = David } is ignored.


The generated URL path is Alice/Bob/Carol/Donovan .

Warning: URL paths are hierarchical. In the preceding example, if the value { c = Cheryl
} is added:

Both of the values { c = Carol, d = David } are ignored.


There is no longer a value for d and URL generation fails.
The desired values of c and d must be specified to generate a URL.

You might expect to hit this problem with the default route
{controller}/{action}/{id?} . This problem is rare in practice because Url.Action

always explicitly specifies a controller and action value.

Several overloads of Url.Action take a route values object to provide values for route
parameters other than controller and action . The route values object is frequently
used with id . For example, Url.Action("Buy", "Products", new { id = 17 }) . The route
values object:

By convention is usually an object of anonymous type.


Can be an IDictionary<> or a POCO ).

Any additional route values that don't match route parameters are put in the query
string.

C#

public IActionResult Index()


{
var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
return Content(url!);
}

The preceding code generates /Products/Buy/17?color=red .

The following code generates an absolute URL:

C#
public IActionResult Index2()
{
var url = Url.Action("Buy", "Products", new { id = 17 }, protocol:
Request.Scheme);
// Returns https://localhost:5001/Products/Buy/17
return Content(url!);
}

To create an absolute URL, use one of the following:

An overload that accepts a protocol . For example, the preceding code.


LinkGenerator.GetUriByAction, which generates absolute URIs by default.

Generate URLs by route


The preceding code demonstrated generating a URL by passing in the controller and
action name. IUrlHelper also provides the Url.RouteUrl family of methods. These
methods are similar to Url.Action, but they don't copy the current values of action and
controller to the route values. The most common usage of Url.RouteUrl :

Specifies a route name to generate the URL.


Generally doesn't specify a controller or action name.

C#

public class UrlGeneration2Controller : Controller


{
[HttpGet("")]
public IActionResult Source()
{
var url = Url.RouteUrl("Destination_Route");
return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
}

[HttpGet("custom/url/to/destination2", Name = "Destination_Route")]


public IActionResult Destination()
{
return ControllerContext.MyDisplayRouteInfo();
}

The following Razor file generates an HTML link to the Destination_Route :

CSHTML

<h1>Test Links</h1>

<ul>
<li><a href="@Url.RouteUrl("Destination_Route")">Test
Destination_Route</a></li>
</ul>

Generate URLs in HTML and Razor


IHtmlHelper provides the HtmlHelper methods Html.BeginForm and Html.ActionLink to
generate <form> and <a> elements respectively. These methods use the Url.Action
method to generate a URL and they accept similar arguments. The Url.RouteUrl
companions for HtmlHelper are Html.BeginRouteForm and Html.RouteLink which have
similar functionality.

TagHelpers generate URLs through the form TagHelper and the <a> TagHelper. Both of
these use IUrlHelper for their implementation. See Tag Helpers in forms for more
information.

Inside views, the IUrlHelper is available through the Url property for any ad-hoc URL
generation not covered by the above.

URL generation in Action Results


The preceding examples showed using IUrlHelper in a controller. The most common
usage in a controller is to generate a URL as part of an action result.

The ControllerBase and Controller base classes provide convenience methods for action
results that reference another action. One typical usage is to redirect after accepting
user input:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, Customer customer)
{
if (ModelState.IsValid)
{
// Update DB with new details.
ViewData["Message"] = $"Successful edit of customer {id}";
return RedirectToAction("Index");
}
return View(customer);
}
The action results factory methods such as RedirectToAction and CreatedAtAction follow
a similar pattern to the methods on IUrlHelper .

Special case for dedicated conventional routes


Conventional routing can use a special kind of route definition called a dedicated
conventional route. In the following example, the route named blog is a dedicated
conventional route:

C#

app.MapControllerRoute(name: "blog",
pattern: "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

Using the preceding route definitions, Url.Action("Index", "Home") generates the URL
path / using the default route, but why? You might guess the route values {
controller = Home, action = Index } would be enough to generate a URL using blog ,

and the result would be /blog?action=Index&controller=Home .

Dedicated conventional routes rely on a special behavior of default values that don't
have a corresponding route parameter that prevents the route from being too greedy
with URL generation. In this case the default values are { controller = Blog, action =
Article } , and neither controller nor action appears as a route parameter. When

routing performs URL generation, the values provided must match the default values.
URL generation using blog fails because the values { controller = Home, action =
Index } don't match { controller = Blog, action = Article } . Routing then falls back
to try default , which succeeds.

Areas
Areas are an MVC feature used to organize related functionality into a group as a
separate:

Routing namespace for controller actions.


Folder structure for views.

Using areas allows an app to have multiple controllers with the same name, as long as
they have different areas. Using areas creates a hierarchy for the purpose of routing by
adding another route parameter, area to controller and action . This section discusses
how routing interacts with areas. See Areas for details about how areas are used with
views.

The following example configures MVC to use the default conventional route and an
area route for an area named Blog :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapAreaControllerRoute("blog_route", "Blog",
"Manage/{controller}/{action}/{id?}");
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");

app.Run();

In the preceding code, MapAreaControllerRoute is called to create the "blog_route" .


The second parameter, "Blog" , is the area name.

When matching a URL path like /Manage/Users/AddUser , the "blog_route" route


generates the route values { area = Blog, controller = Users, action = AddUser } .
The area route value is produced by a default value for area . The route created by
MapAreaControllerRoute is equivalent to the following:

C#

app.MapControllerRoute("blog_route", "Manage/{controller}/{action}/{id?}",
defaults: new { area = "Blog" }, constraints: new { area = "Blog"
});
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");
MapAreaControllerRoute creates a route using both a default value and constraint for

area using the provided area name, in this case Blog . The default value ensures that the
route always produces { area = Blog, ... } , the constraint requires the value { area =
Blog, ... } for URL generation.

Conventional routing is order-dependent. In general, routes with areas should be placed


earlier as they're more specific than routes without an area.

Using the preceding example, the route values { area = Blog, controller = Users,
action = AddUser } match the following action:

C#

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
// GET /manage/users/adduser
public IActionResult AddUser()
{
var area =
ControllerContext.ActionDescriptor.RouteValues["area"];
var actionName = ControllerContext.ActionDescriptor.ActionName;
var controllerName =
ControllerContext.ActionDescriptor.ControllerName;

return Content($"area name:{area}" +


$" controller:{controllerName} action name: {actionName}");
}
}
}

The [Area] attribute is what denotes a controller as part of an area. This controller is in
the Blog area. Controllers without an [Area] attribute are not members of any area, and
do not match when the area route value is provided by routing. In the following
example, only the first controller listed can match the route values { area = Blog,
controller = Users, action = AddUser } .

C#

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
// GET /manage/users/adduser
public IActionResult AddUser()
{
var area =
ControllerContext.ActionDescriptor.RouteValues["area"];
var actionName = ControllerContext.ActionDescriptor.ActionName;
var controllerName =
ControllerContext.ActionDescriptor.ControllerName;

return Content($"area name:{area}" +


$" controller:{controllerName} action name: {actionName}");
}
}
}

C#

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace2
{
// Matches { area = Zebra, controller = Users, action = AddUser }
[Area("Zebra")]
public class UsersController : Controller
{
// GET /zebra/users/adduser
public IActionResult AddUser()
{
var area =
ControllerContext.ActionDescriptor.RouteValues["area"];
var actionName = ControllerContext.ActionDescriptor.ActionName;
var controllerName =
ControllerContext.ActionDescriptor.ControllerName;

return Content($"area name:{area}" +


$" controller:{controllerName} action name: {actionName}");
}
}
}

C#

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace3
{
// Matches { area = string.Empty, controller = Users, action = AddUser }
// Matches { area = null, controller = Users, action = AddUser }
// Matches { controller = Users, action = AddUser }
public class UsersController : Controller
{
// GET /users/adduser
public IActionResult AddUser()
{
var area =
ControllerContext.ActionDescriptor.RouteValues["area"];
var actionName = ControllerContext.ActionDescriptor.ActionName;
var controllerName =
ControllerContext.ActionDescriptor.ControllerName;

return Content($"area name:{area}" +


$" controller:{controllerName} action name: {actionName}");
}
}
}

The namespace of each controller is shown here for completeness. If the preceding
controllers used the same namespace, a compiler error would be generated. Class
namespaces have no effect on MVC's routing.

The first two controllers are members of areas, and only match when their respective
area name is provided by the area route value. The third controller isn't a member of
any area, and can only match when no value for area is provided by routing.

In terms of matching no value, the absence of the area value is the same as if the value
for area were null or the empty string.

When executing an action inside an area, the route value for area is available as an
ambient value for routing to use for URL generation. This means that by default areas
act sticky for URL generation as demonstrated by the following sample.

C#

app.MapAreaControllerRoute(name: "duck_route",
areaName: "Duck",
pattern:
"Manage/{controller}/{action}/{id?}");
app.MapControllerRoute(name: "default",
pattern:
"Manage/{controller=Home}/{action=Index}/{id?}");

C#

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace4
{
[Area("Duck")]
public class UsersController : Controller
{
// GET /Manage/users/GenerateURLInArea
public IActionResult GenerateURLInArea()
{
// Uses the 'ambient' value of area.
var url = Url.Action("Index", "Home");
// Returns /Manage/Home/Index
return Content(url);
}

// GET /Manage/users/GenerateURLOutsideOfArea
public IActionResult GenerateURLOutsideOfArea()
{
// Uses the empty value for area.
var url = Url.Action("Index", "Home", new { area = "" });
// Returns /Manage
return Content(url);
}
}
}

The following code generates a URL to /Zebra/Users/AddUser :

C#

public class HomeController : Controller


{
public IActionResult About()
{
var url = Url.Action("AddUser", "Users", new { Area = "Zebra" });
return Content($"URL: {url}");
}

Action definition
Public methods on a controller, except those with the NonAction attribute, are actions.

Sample code
MyDisplayRouteInfo is provided by the Rick.Docs.Samples.RouteInfo NuGet
package and displays route information.
View or download sample code (how to download)

Debug diagnostics
For detailed routing diagnostic output, set Logging:LogLevel:Microsoft to Debug . In the
development environment, set the log level in appsettings.Development.json :

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Dependency injection into controllers in
ASP.NET Core
Article • 04/25/2023

By Shadi Alnamrouti , Rick Anderson , and Steve Smith

ASP.NET Core MVC controllers request dependencies explicitly via constructors. ASP.NET
Core has built-in support for dependency injection (DI). DI makes apps easier to test and
maintain.

View or download sample code (how to download)

Constructor injection
Services are added as a constructor parameter, and the runtime resolves the service
from the service container. Services are typically defined using interfaces. For example,
consider an app that requires the current time. The following interface exposes the
IDateTime service:

C#

public interface IDateTime


{
DateTime Now { get; }
}

The following code implements the IDateTime interface:

C#

public class SystemDateTime : IDateTime


{
public DateTime Now
{
get { return DateTime.Now; }
}
}

Add the service to the service container:

C#
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IDateTime, SystemDateTime>();

services.AddControllersWithViews();
}

For more information on AddSingleton, see DI service lifetimes.

The following code displays a greeting to the user based on the time of day:

C#

public class HomeController : Controller


{
private readonly IDateTime _dateTime;

public HomeController(IDateTime dateTime)


{
_dateTime = dateTime;
}

public IActionResult Index()


{
var serverTime = _dateTime.Now;
if (serverTime.Hour < 12)
{
ViewData["Message"] = "It's morning here - Good Morning!";
}
else if (serverTime.Hour < 17)
{
ViewData["Message"] = "It's afternoon here - Good Afternoon!";
}
else
{
ViewData["Message"] = "It's evening here - Good Evening!";
}
return View();
}

Run the app and a message is displayed based on the time.

Action injection with FromServices


The FromServicesAttribute enables injecting a service directly into an action method
without using constructor injection:

C#
public IActionResult About([FromServices] IDateTime dateTime)
{
return Content( $"Current server time: {dateTime.Now}");
}

Access settings from a controller


Accessing app or configuration settings from within a controller is a common pattern.
The options pattern described in Options pattern in ASP.NET Core is the preferred
approach to manage settings. Generally, don't directly inject IConfiguration into a
controller.

Create a class that represents the options. For example:

C#

public class SampleWebSettings


{
public string Title { get; set; }
public int Updates { get; set; }
}

Add the configuration class to the services collection:

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddSingleton<IDateTime, SystemDateTime>();
services.Configure<SampleWebSettings>(Configuration);

services.AddControllersWithViews();
}

Configure the app to read the settings from a JSON-formatted file:

C#

public class Program


{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

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


Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("samplewebsettings.json",
optional: false,
reloadOnChange: true);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

The following code requests the IOptions<SampleWebSettings> settings from the service
container and uses them in the Index method:

C#

public class SettingsController : Controller


{
private readonly SampleWebSettings _settings;

public SettingsController(IOptions<SampleWebSettings> settingsOptions)


{
_settings = settingsOptions.Value;
}

public IActionResult Index()


{
ViewData["Title"] = _settings.Title;
ViewData["Updates"] = _settings.Updates;
return View();
}
}

Additional resources
See Test controller logic in ASP.NET Core to learn how to make code easier to test
by explicitly requesting dependencies in controllers.

Replace the default dependency injection container with a third party


implementation.
Dependency injection into views in
ASP.NET Core
Article • 06/08/2022

ASP.NET Core supports dependency injection into views. This can be useful for view-
specific services, such as localization or data required only for populating view elements.
Most of the data views display should be passed in from the controller.

View or download sample code (how to download)

Configuration injection
The values in settings files, such as appsettings.json and
appsettings.Development.json , can be injected into a view. Consider the

appsettings.Development.json from the sample code :

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"MyRoot": {
"MyParent": {
"MyChildName": "Joe"
}
}
}

The following markup displays the configuration value in a Razor Pages view:

CSHTML

@page
@model PrivacyModel
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
ViewData["Title"] = "Privacy RP";
}
<h1>@ViewData["Title"]</h1>
<p>PR Privacy</p>

<h2>
MyRoot:MyParent:MyChildName:
@Configuration["MyRoot:MyParent:MyChildName"]
</h2>

The following markup displays the configuration value in a MVC view:

CSHTML

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
ViewData["Title"] = "Privacy MVC";
}
<h1>@ViewData["Title"]</h1>

<p>MVC Use this page to detail your site's privacy policy.</p>

<h2>
MyRoot:MyParent:MyChildName:
@Configuration["MyRoot:MyParent:MyChildName"]
</h2>

For more information, see Configuration in ASP.NET Core

Service injection
A service can be injected into a view using the @inject directive.

CSHTML

@using System.Threading.Tasks
@using ViewInjectSample.Model
@using ViewInjectSample.Model.Services
@model IEnumerable<ToDoItem>
@inject StatisticsService StatsService
<!DOCTYPE html>
<html>
<head>
<title>To Do Items</title>
</head>
<body>
<div>
<h1>To Do Items</h1>
<ul>
<li>Total Items: @StatsService.GetCount()</li>
<li>Completed: @StatsService.GetCompletedCount()</li>
<li>Avg. Priority: @StatsService.GetAveragePriority()</li>
</ul>
<table>
<tr>
<th>Name</th>
<th>Priority</th>
<th>Is Done?</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>@item.Name</td>
<td>@item.Priority</td>
<td>@item.IsDone</td>
</tr>
}
</table>
</div>
</body>
</html>

This view displays a list of ToDoItem instances, along with a summary showing overall
statistics. The summary is populated from the injected StatisticsService . This service is
registered for dependency injection in ConfigureServices in Program.cs :

C#

using ViewInjectSample.Helpers;
using ViewInjectSample.Infrastructure;
using ViewInjectSample.Interfaces;
using ViewInjectSample.Model.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IToDoItemRepository, ToDoItemRepository>();
builder.Services.AddTransient<StatisticsService>();
builder.Services.AddTransient<ProfileOptionsService>();
builder.Services.AddTransient<MyHtmlHelper>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.MapRazorPages();

app.MapDefaultControllerRoute();

app.Run();

The StatisticsService performs some calculations on the set of ToDoItem instances,


which it accesses via a repository:

C#

using System.Linq;
using ViewInjectSample.Interfaces;

namespace ViewInjectSample.Model.Services
{
public class StatisticsService
{
private readonly IToDoItemRepository _toDoItemRepository;

public StatisticsService(IToDoItemRepository toDoItemRepository)


{
_toDoItemRepository = toDoItemRepository;
}

public int GetCount()


{
return _toDoItemRepository.List().Count();
}

public int GetCompletedCount()


{
return _toDoItemRepository.List().Count(x => x.IsDone);
}

public double GetAveragePriority()


{
if (_toDoItemRepository.List().Count() == 0)
{
return 0.0;
}

return _toDoItemRepository.List().Average(x => x.Priority);


}
}
}
The sample repository uses an in-memory collection. An in-memory implementation
shouldn't be used for large, remotely accessed data sets.

The sample displays data from the model bound to the view and the service injected
into the view:

Populating Lookup Data


View injection can be useful to populate options in UI elements, such as dropdown lists.
Consider a user profile form that includes options for specifying gender, state, and other
preferences. Rendering such a form using a standard approach might require the
controller or Razor Page to:

Request data access services for each of the sets of options.


Populate a model or ViewBag with each set of options to be bound.

An alternative approach injects services directly into the view to obtain the options. This
minimizes the amount of code required by the controller or razor Page, moving this
view element construction logic into the view itself. The controller action or Razor Page
to display a profile editing form only needs to pass the form the profile instance:

C#

using Microsoft.AspNetCore.Mvc;
using ViewInjectSample.Model;

namespace ViewInjectSample.Controllers;

public class ProfileController : Controller


{
public IActionResult Index()
{
// A real app would up profile based on the user.
var profile = new Profile()
{
Name = "Rick",
FavColor = "Blue",
Gender = "Male",
State = new State("Ohio","OH")
};
return View(profile);
}
}

The HTML form used to update the preferences includes dropdown lists for three of the
properties:

These lists are populated by a service that has been injected into the view:

CSHTML

@using System.Threading.Tasks
@using ViewInjectSample.Model.Services
@model ViewInjectSample.Model.Profile
@inject ProfileOptionsService Options
<!DOCTYPE html>
<html>
<head>
<title>Update Profile</title>
</head>
<body>
<div>
<h1>Update Profile</h1>
Name: @Html.TextBoxFor(m => m.Name)
<br/>
Gender: @Html.DropDownList("Gender",
Options.ListGenders().Select(g =>
new SelectListItem() { Text = g, Value = g }))
<br/>
State: @Html.DropDownListFor(m => m.State!.Code,
Options.ListStates().Select(s =>
new SelectListItem() { Text = s.Name, Value = s.Code}))
<br />

Fav. Color: @Html.DropDownList("FavColor",


Options.ListColors().Select(c =>
new SelectListItem() { Text = c, Value = c }))
</div>
</body>
</html>

The ProfileOptionsService is a UI-level service designed to provide just the data


needed for this form:

C#

namespace ViewInjectSample.Model.Services;

public class ProfileOptionsService


{
public List<string> ListGenders()
{
// Basic sample
return new List<string>() {"Female", "Male"};
}

public List<State> ListStates()


{
// Add a few states
return new List<State>()
{
new State("Alabama", "AL"),
new State("Alaska", "AK"),
new State("Ohio", "OH")
};
}

public List<string> ListColors()


{
return new List<string>() { "Blue","Green","Red","Yellow" };
}
}

Note an unregistered type throws an exception at runtime because the service provider
is internally queried via GetRequiredService.

Overriding Services
In addition to injecting new services, this technique can be used to override previously
injected services on a page. The figure below shows all of the fields available on the
page used in the first example:

The default fields include Html , Component , and Url . To replace the default HTML
Helpers with a custom version, use @inject :

CSHTML

@using System.Threading.Tasks
@using ViewInjectSample.Helpers
@inject MyHtmlHelper Html
<!DOCTYPE html>
<html>
<head>
<title>My Helper</title>
</head>
<body>
<div>
Test: @Html.Value
</div>
</body>
</html>

See Also
Simon Timms Blog: Getting Lookup Data Into Your View
Unit test controller logic in ASP.NET
Core
Article • 09/27/2022

By Steve Smith

Unit tests involve testing a part of an app in isolation from its infrastructure and
dependencies. When unit testing controller logic, only the contents of a single action are
tested, not the behavior of its dependencies or of the framework itself.

Unit testing controllers


Set up unit tests of controller actions to focus on the controller's behavior. A controller
unit test avoids scenarios such as filters, routing, and model binding. Tests that cover the
interactions among components that collectively respond to a request are handled by
integration tests. For more information on integration tests, see Integration tests in
ASP.NET Core.

If you're writing custom filters and routes, unit test them in isolation, not as part of tests
on a particular controller action.

To demonstrate controller unit tests, review the following controller in the sample app.

View or download sample code (how to download)

The Home controller displays a list of brainstorming sessions and allows the creation of
new brainstorming sessions with a POST request:

C#

public class HomeController : Controller


{
private readonly IBrainstormSessionRepository _sessionRepository;

public HomeController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

public async Task<IActionResult> Index()


{
var sessionList = await _sessionRepository.ListAsync();

var model = sessionList.Select(session => new


StormSessionViewModel()
{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});

return View(model);
}

public class NewSessionModel


{
[Required]
public string SessionName { get; set; }
}

[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}

return RedirectToAction(actionName: nameof(Index));


}
}

The preceding controller:

Follows the Explicit Dependencies Principle.


Expects dependency injection (DI) to provide an instance of
IBrainstormSessionRepository .

Can be tested with a mocked IBrainstormSessionRepository service using a mock


object framework, such as Moq . A mocked object is a fabricated object with a
predetermined set of property and method behaviors used for testing. For more
information, see Introduction to integration tests.

The HTTP GET Index method has no looping or branching and only calls one method.
The unit test for this action:
Mocks the IBrainstormSessionRepository service using the GetTestSessions
method. GetTestSessions creates two mock brainstorm sessions with dates and
session names.
Executes the Index method.
Makes assertions on the result returned by the method:
A ViewResult is returned.
The ViewDataDictionary.Model is a StormSessionViewModel .
There are two brainstorming sessions stored in the ViewDataDictionary.Model .

C#

[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);

// Act
var result = await controller.Index();

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}

C#

private List<BrainstormSession> GetTestSessions()


{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
The Home controller's HTTP POST Index method tests verifies that:

When ModelState.IsValid is false , the action method returns a 400 Bad Request
ViewResult with the appropriate data.
When ModelState.IsValid is true :
The Add method on the repository is called.
A RedirectToActionResult is returned with the correct arguments.

An invalid model state is tested by adding errors using AddModelError as shown in the
first test below:

C#

[Fact]
public async Task
IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();

// Act
var result = await controller.Index(newSession);

// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}

[Fact]
public async Task
IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};

// Act
var result = await controller.Index(newSession);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>
(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}

When ModelState isn't valid, the same ViewResult is returned as for a GET request. The
test doesn't attempt to pass in an invalid model. Passing an invalid model isn't a valid
approach, since model binding isn't running (although an integration test does use
model binding). In this case, model binding isn't tested. These unit tests are only testing
the code in the action method.

The second test verifies that when the ModelState is valid:

A new BrainstormSession is added (via the repository).


The method returns a RedirectToActionResult with the expected properties.

Mocked calls that aren't called are normally ignored, but calling Verifiable at the end
of the setup call allows mock validation in the test. This is performed with the call to
mockRepo.Verify , which fails the test if the expected method wasn't called.

7 Note

The Moq library used in this sample makes it possible to mix verifiable, or "strict",
mocks with non-verifiable mocks (also called "loose" mocks or stubs). Learn more
about customizing Mock behavior with Moq .

SessionController in the sample app displays information related to a particular


brainstorming session. The controller includes logic to deal with invalid id values (there
are two return scenarios in the following example to cover these scenarios). The final
return statement returns a new StormSessionViewModel to the view
( Controllers/SessionController.cs ):

C#

public class SessionController : Controller


{
private readonly IBrainstormSessionRepository _sessionRepository;

public SessionController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index(int? id)
{
if (!id.HasValue)
{
return RedirectToAction(actionName: nameof(Index),
controllerName: "Home");
}

var session = await _sessionRepository.GetByIdAsync(id.Value);


if (session == null)
{
return Content("Session not found.");
}

var viewModel = new StormSessionViewModel()


{
DateCreated = session.DateCreated,
Name = session.Name,
Id = session.Id
};

return View(viewModel);
}
}

The unit tests include one test for each return scenario in the Session controller Index
action:

C#

[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);

// Act
var result = await controller.Index(id: null);

// Assert
var redirectToActionResult =
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}

[Fact]
public async Task
IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new SessionController(mockRepo.Object);

// Act
var result = await controller.Index(testSessionId);

// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}

[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSessions().FirstOrDefault(
s => s.Id == testSessionId));
var controller = new SessionController(mockRepo.Object);

// Act
var result = await controller.Index(testSessionId);

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(
viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}

Moving to the Ideas controller, the app exposes functionality as a web API on the
api/ideas route:

A list of ideas ( IdeaDTO ) associated with a brainstorming session is returned by the


ForSession method.

The Create method adds new ideas to a session.

C#

[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}

var result = session.Ideas.Select(idea => new IdeaDTO()


{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();

return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var session = await _sessionRepository.GetByIdAsync(model.SessionId);


if (session == null)
{
return NotFound(model.SessionId);
}

var idea = new Idea()


{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);

await _sessionRepository.UpdateAsync(session);

return Ok(session);
}

Avoid returning business domain entities directly via API calls. Domain entities:

Often include more data than the client requires.


Unnecessarily couple the app's internal domain model with the publicly exposed
API.

Mapping between domain entities and the types returned to the client can be
performed:

Manually with a LINQ Select , as the sample app uses. For more information, see
LINQ (Language Integrated Query).
Automatically with a library, such as AutoMapper .

Next, the sample app demonstrates unit tests for the Create and ForSession API
methods of the Ideas controller.

The sample app contains two ForSession tests. The first test determines if ForSession
returns a NotFoundObjectResult (HTTP Not Found) for an invalid session:

C#

[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.ForSession(testSessionId);

// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}

The second ForSession test determines if ForSession returns a list of session ideas
( <List<IdeaDTO>> ) for a valid session. The checks also examine the first idea to confirm
its Name property is correct:

C#

[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.ForSession(testSessionId);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}

To test the behavior of the Create method when the ModelState is invalid, the sample
app adds a model error to the controller as part of the test. Don't try to test model
validation or model binding in unit tests—just test the action method's behavior when
confronted with an invalid ModelState :

C#

[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");

// Act
var result = await controller.Create(model: null);

// Assert
Assert.IsType<BadRequestObjectResult>(result);
}

The second test of Create depends on the repository returning null , so the mock
repository is configured to return null . There's no need to create a test database (in
memory or otherwise) and construct a query that returns this result. The test can be
accomplished in a single statement, as the sample code illustrates:

C#

[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.Create(new NewIdeaModel());

// Assert
Assert.IsType<NotFoundObjectResult>(result);
}

The third Create test, Create_ReturnsNewlyCreatedIdeaForSession , verifies that the


repository's UpdateAsync method is called. The mock is called with Verifiable , and the
mocked repository's Verify method is called to confirm the verifiable method is
executed. It's not the unit test's responsibility to ensure that the UpdateAsync method
saved the data—that can be performed with an integration test.

C#

[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);

var newIdea = new NewIdeaModel()


{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();

// Act
var result = await controller.Create(newIdea);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription,
returnSession.Ideas.LastOrDefault().Description);
}

Test ActionResult<T>
ActionResult<T> (ActionResult<TValue>) can return a type deriving from ActionResult
or return a specific type.

The sample app includes a method that returns a List<IdeaDTO> for a given session id .
If the session id doesn't exist, the controller returns NotFound:

C#

[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int
sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);

if (session == null)
{
return NotFound(sessionId);
}

var result = session.Ideas.Select(idea => new IdeaDTO()


{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();

return result;
}

Two tests of the ForSessionActionResult controller are included in the


ApiIdeasControllerTests .

The first test confirms that the controller returns an ActionResult but not a nonexistent
list of ideas for a nonexistent session id :

The ActionResult type is ActionResult<List<IdeaDTO>> .


The Result is a NotFoundObjectResult.

C#

[Fact]
public async Task
ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;

// Act
var result = await
controller.ForSessionActionResult(nonExistentSessionId);

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

For a valid session id , the second test confirms that the method returns:

An ActionResult with a List<IdeaDTO> type.


The ActionResult<T>.Value is a List<IdeaDTO> type.
The first item in the list is a valid idea matching the idea stored in the mock session
(obtained by calling GetTestSession ).

C#

[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.ForSessionActionResult(testSessionId);

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}

The sample app also includes a method to create a new Idea for a given session. The
controller returns:

BadRequest for an invalid model.


NotFound if the session doesn't exist.
CreatedAtAction when the session is updated with the new idea.

C#
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>>
CreateActionResult([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var session = await _sessionRepository.GetByIdAsync(model.SessionId);

if (session == null)
{
return NotFound(model.SessionId);
}

var idea = new Idea()


{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);

await _sessionRepository.UpdateAsync(session);

return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id


}, session);
}

Three tests of CreateActionResult are included in the ApiIdeasControllerTests .

The first text confirms that a BadRequest is returned for an invalid model.

C#

[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");

// Act
var result = await controller.CreateActionResult(model: null);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>
(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}

The second test checks that a NotFound is returned if the session doesn't exist.

C#

[Fact]
public async Task
CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);

var newIdea = new NewIdeaModel()


{
Description = testDescription,
Name = testName,
SessionId = nonExistentSessionId
};

// Act
var result = await controller.CreateActionResult(newIdea);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>
(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

For a valid session id , the final test confirms that:

The method returns an ActionResult with a BrainstormSession type.


The ActionResult<T>.Result is a CreatedAtActionResult. CreatedAtActionResult is
analogous to a 201 Created response with a Location header.
The ActionResult<T>.Value is a BrainstormSession type.
The mock call to update the session, UpdateAsync(testSession) , was invoked. The
Verifiable method call is checked by executing mockRepo.Verify() in the
assertions.
Two Idea objects are returned for the session.
The last item (the Idea added by the mock call to UpdateAsync ) matches the
newIdea added to the session in the test.
C#

[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);

var newIdea = new NewIdeaModel()


{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();

// Act
var result = await controller.CreateActionResult(newIdea);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>
(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>
(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>
(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription,
returnValue.Ideas.LastOrDefault().Description);
}

Additional resources
Integration tests in ASP.NET Core
Create and run unit tests with Visual Studio
MyTested.AspNetCore.Mvc - Fluent Testing Library for ASP.NET Core MVC :
Strongly-typed unit testing library, providing a fluent interface for testing MVC and
web API apps. (Not maintained or supported by Microsoft.)
JustMockLite : A mocking framework for .NET developers. (Not maintained or
supported by Microsoft.)
ASP.NET Core Blazor
Article • 02/24/2023

Welcome to Blazor!

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.

7 Note

For a Blazor quick start tutorial, see Build your first Blazor app .

Components
Blazor apps are based on components. A component in Blazor is an element of UI, such
as a page, dialog, or data entry form.

Components are .NET C# classes built into .NET assemblies that:

Define flexible UI rendering logic.


Handle user events.
Can be nested and reused.
Can be shared and distributed as Razor class libraries or NuGet packages.
The component class is usually written in the form of a Razor markup page with a
.razor file extension. Components in Blazor are formally referred to as Razor
components, informally as Blazor components. Razor is a syntax for combining HTML
markup with C# code designed for developer productivity. Razor allows you to switch
between HTML markup and C# in the same file with IntelliSense programming support
in Visual Studio. Razor Pages and MVC also use Razor. Unlike Razor Pages and MVC,
which are built around a request/response model, components are used specifically for
client-side UI logic and composition.

Blazor uses natural HTML tags for UI composition. The following Razor markup
demonstrates a component ( Dialog.razor ) that displays a dialog and processes an
event when the user selects a button:

razor

<div class="card" style="width:22rem">


<div class="card-body">
<h3 class="card-title">@Title</h3>
<p class="card-text">@ChildContent</p>
<button @onclick="OnYes">Yes!</button>
</div>
</div>

@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }

[Parameter]
public string? Title { get; set; }

private void OnYes()


{
Console.WriteLine("Write to the console in C#! 'Yes' button
selected.");
}
}

In the preceding example, OnYes is a C# method triggered by the button's onclick


event. The dialog's text ( ChildContent ) and title ( Title ) are provided by the following
component that uses this component in its UI.

The Dialog component is nested within another component using an HTML tag. In the
following example, the Index component ( Pages/Index.razor ) uses the preceding
Dialog component. The tag's Title attribute passes a value for the title to the Dialog

component's Title property. The Dialog component's text ( ChildContent ) are set by
the content of the <Dialog> element. When the Dialog component is added to the
Index component, IntelliSense in Visual Studio speeds development with syntax and

parameter completion.

razor

@page "/"

<h1>Hello, world!</h1>

<p>
Welcome to your new app.
</p>

<Dialog Title="Learn More">


Do you want to <i>learn more</i> about Blazor?
</Dialog>

The dialog is rendered when the Index component is accessed in a browser. When the
button is selected by the user, the browser's developer tools console shows the
message written by the OnYes method:

Components render into an in-memory representation of the browser's Document


Object Model (DOM) called a render tree, which is used to update the UI in a flexible
and efficient way.
Blazor Server
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.

The runtime stays on the server and handles:

Executing the app's C# code.


Sending UI events from the browser to the server.
Applying UI updates to a rendered component that are sent back by the server.

The connection used by Blazor Server to communicate with the browser is also used to
handle JavaScript interop calls.

Blazor Server apps render content differently than traditional models for rendering UI in
ASP.NET Core apps using Razor views or Razor Pages. Both models use the Razor
language to describe HTML content for rendering, but they significantly differ in how
markup is rendered.

When a Razor Page or view is rendered, every line of Razor code emits HTML in text
form. After rendering, the server disposes of the page or view instance, including any
state that was produced. When another request for the page occurs, the entire page is
rerendered to HTML again and sent to the client.

Blazor Server produces a graph of components to display similar to an HTML or XML


Document Object Model (DOM) . The component graph includes state held in
properties and fields. Blazor evaluates the component graph to produce a binary
representation of the markup, which is sent to the client for rendering. After the
connection is made between the client and the server, the component's static
prerendered elements are replaced with interactive elements. Prerendering the content
on the server makes the app feel more responsive on the client.

After the components are interactive on the client, UI updates are triggered by user
interaction and app events. When an update occurs, the component graph is
rerendered, and a UI diff (difference) is calculated. This diff is the smallest set of DOM
edits required to update the UI on the client. The diff is sent to the client in a binary
format and applied by the browser.

A component is disposed after the user navigates away from the component.

Blazor WebAssembly
Blazor WebAssembly is a single-page app (SPA) framework for building interactive
client-side web apps with .NET.

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. WebAssembly works in all modern web
browsers, including mobile browsers.

WebAssembly code can access the full functionality of the browser via JavaScript, called
JavaScript interoperability, often shortened to JavaScript interop or JS interop. .NET code
executed via WebAssembly in the browser runs in the browser's JavaScript sandbox with
the protections that the sandbox provides against malicious actions on the client
machine.
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.

The size of the published app, its payload size, is a critical performance factor for an
app's usability. A large app takes a relatively long time to download to a browser, which
diminishes the user experience. Blazor WebAssembly optimizes payload size to reduce
download times:

Unused code is stripped out of the app when it's published by the Intermediate
Language (IL) Trimmer.
HTTP responses are compressed.
The .NET runtime and assemblies are cached in the browser.

Blazor Hybrid
Hybrid apps use a blend of native and web technologies. A Blazor Hybrid app uses
Blazor in a native client app. Razor components run natively in the .NET process and
render web UI to an embedded Web View control using a local interop channel.
WebAssembly isn't used in Hybrid apps. Hybrid apps encompass the following
technologies:

.NET Multi-platform App UI (.NET MAUI): A cross-platform framework for creating


native mobile and desktop apps with C# and XAML.
Windows Presentation Foundation (WPF): A UI framework that is resolution-
independent and uses a vector-based rendering engine, built to take advantage of
modern graphics hardware.
Windows Forms: A UI framework that creates rich desktop client apps for
Windows. The Windows Forms development platform supports a broad set of app
development features, including controls, graphics, data binding, and user input.

For more information on creating Blazor Hybrid apps with the preceding frameworks,
see the following articles:

ASP.NET Core Blazor hosting models


ASP.NET Core Blazor Hybrid

JavaScript interop
For apps that require third-party JavaScript libraries and access to browser APIs,
components interoperate with JavaScript. Components are capable of using any library
or API that JavaScript is able to use. C# code can call into JavaScript code, and JavaScript
code can call into C# code.

Code sharing and .NET Standard


Blazor implements the .NET Standard, which enables Blazor projects to reference
libraries that conform to .NET Standard specifications. .NET Standard is a formal
specification of .NET APIs that are common across .NET implementations. .NET Standard
class libraries can be shared across different .NET platforms, such as Blazor, .NET
Framework, .NET Core, Xamarin, Mono, and Unity.

APIs that aren't applicable inside of a web browser (for example, accessing the file
system, opening a socket, and threading) throw a PlatformNotSupportedException.

Additional resources
WebAssembly
ASP.NET Core Blazor hosting models
Use ASP.NET Core SignalR with Blazor
Call JavaScript functions from .NET methods in ASP.NET Core Blazor
Call .NET methods from JavaScript functions in ASP.NET Core Blazor
mono/mono GitHub repository
C# Guide
Razor syntax reference for ASP.NET Core
HTML
Awesome Blazor (Links to community-maintained Blazor resources)
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor supported
platforms
Article • 04/04/2023

Blazor WebAssembly and Blazor Server are supported in the browsers shown in the
following table on both mobile and desktop platforms.

Browser Version

Apple Safari Current†

Google Chrome Current†

Microsoft Edge Current†

Mozilla Firefox Current†

†Current refers to the latest version of the browser.

For Blazor Hybrid apps, we test on and support the latest platform Web View control
versions:

Microsoft Edge WebView2 on Windows


Chrome on Android
Safari on iOS and macOS

Additional resources
ASP.NET Core Blazor hosting models
ASP.NET Core SignalR supported platforms
Tooling for ASP.NET Core Blazor
Article • 06/22/2023

This article describes tools for building Blazor apps on various platforms. Select your
platform at the top of this article.

To create a Blazor app on Windows, use the following guidance:

Install the latest version of Visual Studio with the ASP.NET and web
development workload.

Create a new project:


For a Blazor Server experience, choose the Blazor Server App template, which
includes demonstration code and Bootstrap , or the Blazor Server App Empty
template without demonstration code and Bootstrap. Select Next.
For a Blazor WebAssembly experience, choose the Blazor WebAssembly App
template, which includes demonstration code and Bootstrap, or the Blazor
WebAssembly App Empty template without demonstration code and
Bootstrap. Select Next.

Provide a Project name and confirm that the Location is correct. Select Next.

For a hosted Blazor WebAssembly app, select the ASP.NET Core Hosted checkbox
in the Additional information dialog.

Select Create.

Press Ctrl + F5 (Windows) or ⌘ + F5 (macOS) to run the app.

When running a hosted Blazor WebAssembly solution in Visual Studio, the startup
project of the solution is the Server project.

For more information on trusting the ASP.NET Core HTTPS development certificate, see
Enforce HTTPS in ASP.NET Core.

) Important

When executing a hosted Blazor WebAssembly app, run the app from the solution's
Server project.

When the app is launched, only the Properties/launchSettings.json file in the


Server project is used.
Visual Studio solution file ( .sln )
A solution is a container to organize one or more related code projects. Visual Studio
and Visual Studio for Mac use a solution file ( .sln ) to store settings for a solution.
Solution files use a unique format and aren't intended to be edited directly.

Tooling outside of Visual Studio and Visual Studio for Mac can interact with solution
files:

The .NET CLI can create solution files and list/modify the projects in solution files
via the dotnet sln command. Other .NET CLI commands use the path of the
solution file for various publishing, testing, and packaging commands.
Visual Studio Code can execute the dotnet sln command and other .NET CLI
commands through its integrated terminal but doesn't use the settings in a
solution file directly.

Throughout the Blazor documentation, solution is used to describe apps created from
the Blazor WebAssembly project template with the ASP.NET Core Hosted option
enabled or from a Blazor Hybrid project template. Apps produced from these project
templates include a solution file ( .sln ) by default. For hosted Blazor WebAssembly apps
where the developer isn't using Visual Studio or Visual Studio for Mac, the solution file
can be ignored or deleted if it isn't used with .NET CLI commands.

For more information, see the following resources in the Visual Studio documentation:

Introduction to projects and solutions


What are solutions and projects in Visual Studio?

Use Visual Studio Code for cross-platform


Blazor development
Visual Studio Code is an open source, cross-platform Integrated Development
Environment (IDE) that can be used to develop Blazor apps. Use the .NET CLI to create a
new Blazor app for development with Visual Studio Code. For more information, see the
Linux version of this article.

Blazor template options


The Blazor framework provides templates for creating new apps. The templates are used
to create new Blazor projects and solutions regardless of the tooling that you select for
Blazor development (Visual Studio, Visual Studio for Mac, Visual Studio Code, or the
.NET command-line interface (CLI)):

Blazor Server project templates: blazorserver , blazorserver-empty


Blazor WebAssembly project templates: blazorwasm , blazorwasm-empty

For more information on Blazor project templates, see ASP.NET Core Blazor project
structure.

For more information on template options, see the following resources:

The .NET default templates for dotnet new article in the .NET Core documentation:
blazorserver (includes blazorserver-empty options)
blazorwasm (includes blazorwasm-empty options)
Passing the help option ( -h or --help ) to the dotnet new CLI command in a
command shell:
dotnet new blazorserver -h

dotnet new blazorserver-empty -h


dotnet new blazorwasm -h

dotnet new blazorwasm-empty -h

.NET WebAssembly build tools


The .NET WebAssembly build tools are based on Emscripten , a compiler toolchain for
the web platform. To install the build tools, use either of the following approaches:

For the ASP.NET and web development workload in the Visual Studio installer,
select the .NET WebAssembly build tools option from the list of optional
components.
Execute dotnet workload install wasm-tools in an administrative command shell.

When ahead-of-time (AOT) compilation is used, WebAssembly Single Instruction,


Multiple Data (SIMD) is supported for all major browsers. SIMD can improve the
throughput of vectorized computations by performing an operation on multiple pieces
of data in parallel using a single instruction. Use the <WasmEnableSIMD> property in the
app's project file ( .csproj ) with a value of true :

XML

<PropertyGroup>
<WasmEnableSIMD>true</WasmEnableSIMD>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
To enable WebAssembly exception handling, use the <WasmEnableExceptionHandling>
property in the app's project file ( .csproj ) with a value of true :

XML

<PropertyGroup>
<WasmEnableExceptionHandling>true</WasmEnableExceptionHandling>
</PropertyGroup>

7 Note

.NET WebAssembly build tools for .NET 6 projects

The wasm-tools workload installs the build tools for .NET 7 projects. However, the
.NET 7 version of the build tools are incompatible with existing projects built with
.NET 6. Projects using the build tools that must support both .NET 6 and .NET 7
must use multi-targeting.

Use the wasm-tools-net6 workload for .NET 6 projects when developing apps with
the .NET 7 SDK. To install the wasm-tools-net6 workload, execute the following
command from an administrative command shell:

.NET CLI

dotnet workload install wasm-tools-net6

For more information, see the following resources:

Ahead-of-time (AOT) compilation


Runtime relinking
ASP.NET Core Blazor WebAssembly native dependencies

Additional resources
.NET command-line interface (CLI)
.NET Hot Reload support for ASP.NET Core
ASP.NET Core Blazor hosting models
ASP.NET Core Blazor project structure
ASP.NET Core Blazor Hybrid tutorials
ASP.NET Core Blazor hosting models
Article • 04/04/2023

This article explains the different Blazor hosting models and how to choose which one
to use.

Blazor is a web framework for building web UI components (Razor components) that
can be hosted in different ways. Razor components can run server-side in ASP.NET Core
(Blazor Server) versus client-side in the browser on a WebAssembly -based .NET
runtime (Blazor WebAssembly, Blazor WASM). You can also host Razor components in
native mobile and desktop apps that render to an embedded Web View control (Blazor
Hybrid). Regardless of the hosting model, the way you build Razor components is the
same. The same Razor components can be used with any of the hosting models
unchanged.

Blazor Server
With the Blazor Server hosting model, the app is executed on the server from within an
ASP.NET Core app. UI updates, event handling, and JavaScript calls are handled over a
SignalR connection using the WebSockets protocol. The state on the server associated
with each connected client is called a circuit. Circuits aren't tied to a specific network
connection and can tolerate temporary network interruptions and attempts by the client
to reconnect to the server when the connection is lost.

In a traditional server-rendered app, opening the same app in multiple browser screens
(tabs or iframes ) typically doesn't translate into additional resource demands on the
server. In a Blazor Server app, each browser screen requires a separate circuit and
separate instances of server-managed component state. Blazor considers closing a
browser tab or navigating to an external URL a graceful termination. In the event of a
graceful termination, the circuit and associated resources are immediately released. A
client may also disconnect non-gracefully, for instance due to a network interruption.
Blazor Server stores disconnected circuits for a configurable interval to allow the client
to reconnect.
On the client, the Blazor script ( blazor.server.js ) establishes the SignalR connection
with the server. The script is served to the client-side app from an embedded resource in
the ASP.NET Core shared framework. The client-side app is responsible for persisting
and restoring app state as required.

The Blazor Server hosting model offers several benefits:

Download size is significantly smaller than a Blazor WebAssembly app, and the app
loads much faster.
The app takes full advantage of server capabilities, including the use of .NET Core
APIs.
.NET Core on the server is used to run the app, so existing .NET tooling, such as
debugging, works as expected.
Thin clients are supported. For example, Blazor Server apps work with browsers
that don't support WebAssembly and on resource-constrained devices.
The app's .NET/C# code base, including the app's component code, isn't served to
clients.

The Blazor Server hosting model has the following limitations:

Higher latency usually exists. Every user interaction involves a network hop.
There's no offline support. If the client connection fails, the app stops working.
Scaling apps with many users requires server resources to handle multiple client
connections and client state.
An ASP.NET Core server is required to serve the app. Serverless deployment
scenarios aren't possible, such as serving the app from a Content Delivery Network
(CDN).

We recommend using the Azure SignalR Service for Blazor Server apps. The service
allows for scaling up a Blazor Server app to a large number of concurrent SignalR
connections.

Blazor WebAssembly
Blazor WebAssembly (WASM) apps run client-side in the browser on a WebAssembly-
based .NET runtime. The Blazor app, its dependencies, and the .NET runtime are
downloaded to the browser. The app is executed directly on the browser UI thread. UI
updates and event handling occur within the same process. The app's assets are
deployed as static files to a web server or service capable of serving static content to
clients.

When the Blazor WebAssembly app is created for deployment without a backend
ASP.NET Core app to serve its files, the app is called a standalone Blazor WebAssembly
app. When the app is created for deployment with a backend app to serve its files, the
app is called a hosted Blazor WebAssembly app.
A Blazor WebAssembly app built as a Progressive Web App (PWA) uses modern browser
APIs to enable many of the capabilities of a native client app, such as working offline,
running in its own app window, launching from the host's operating system, receiving
push notifications, and automatically updating in the background.

Using hosted Blazor WebAssembly, you get a full-stack web development experience
with .NET, including the ability to share code between the client and server apps,
support for prerendering, and integration with MVC and Razor Pages. A hosted client
app can interact with its backend server app over the network using a variety of
messaging frameworks and protocols, such as web API, gRPC-web, and SignalR (Use
ASP.NET Core SignalR with Blazor).

The blazor.webassembly.js script is provided by the framework and handles:

Downloading the .NET runtime, the app, and the app's dependencies.
Initialization of the runtime to run the app.

The Blazor WebAssembly (WASM) hosting model offers several benefits:

There's no .NET server-side dependency after the app is downloaded from the
server, so the app remains functional if the server goes offline.
Client resources and capabilities are fully leveraged.
Work is offloaded from the server to the client.
An ASP.NET Core web server isn't required to host the app. Serverless deployment
scenarios are possible, such as serving the app from a Content Delivery Network
(CDN).

The Blazor WebAssembly hosting model has the following limitations:

The app is restricted to the capabilities of the browser.


Capable client hardware and software (for example, WebAssembly support) is
required.
Download size is larger, and apps take longer to load.

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. For more information,
see Host and deploy ASP.NET Core Blazor WebAssembly.

The same .NET WebAssembly build tools used for AOT compilation also relink the .NET
WebAssembly runtime to trim unused runtime code.

Blazor WebAssembly includes support for trimming unused code from .NET Core
framework libraries. For more information, see ASP.NET Core Blazor globalization and
localization. The .NET compiler further precompresses a Blazor WebAssembly app for a
smaller app payload.

Blazor WebAssembly apps can use native dependencies built to run on WebAssembly.

Blazor Hybrid
Blazor can also be used to build native client apps using a hybrid approach. Hybrid apps
are native apps that leverage web technologies for their functionality. In a Blazor Hybrid
app, Razor components run directly in the native app (not on WebAssembly) along with
any other .NET code and render web UI based on HTML and CSS to an embedded Web
View control through a local interop channel.

Blazor Hybrid apps can be built using different .NET native app frameworks, including
.NET MAUI, WPF, and Windows Forms. Blazor provides BlazorWebView controls for
adding Razor components to apps built with these frameworks. Using Blazor with .NET
MAUI offers a convenient way to build cross-platform Blazor Hybrid apps for mobile and
desktop, while Blazor integration with WPF and Windows Forms can be a great way to
modernize existing apps.

Because Blazor Hybrid apps are native apps, they can support functionality that isn't
available with only the web platform. Blazor Hybrid apps have full access to native
platform capabilities through normal .NET APIs. Blazor Hybrid apps can also share and
reuse components with existing Blazor Server or Blazor WebAssembly apps. Blazor
Hybrid apps combine the benefits of the web, native apps, and the .NET platform.

The Blazor Hybrid hosting model offers several benefits:


Reuse existing components that can be shared across mobile, desktop, and web.
Leverage web development skills, experience, and resources.
Apps have full access to the native capabilities of the device.

The Blazor Hybrid hosting model has the following limitations:

Separate native client apps must be built, deployed, and maintained for each
target platform.
Native client apps usually take longer to find, download, and install over accessing
a web app in a browser.

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

For more information on Microsoft native client frameworks, see the following
resources:

.NET Multi-platform App UI (.NET MAUI)


Windows Presentation Foundation (WPF)
Windows Forms

Which Blazor hosting model should I choose?


Select the Blazor hosting model based on the app's feature requirements. The following
table shows the primary considerations for selecting the hosting model.

Blazor Hybrid apps include .NET MAUI, WPF, and Windows Forms framework apps.

Feature Blazor Blazor WebAssembly Blazor


Server (WASM) Hybrid

Complete .NET API compatibility ✔️ ❌ ✔️

Direct access to server and network ✔️ ❌† ❌†


resources

Small payload size with fast initial load ✔️ ❌ ❌


time

Near native execution speed ✔️ ✔️‡ ✔️

App code secure and private on the ✔️ ❌† ❌†


server

Run apps offline once downloaded ❌ ✔️ ✔️

Static site hosting ❌ ✔️ ❌


Feature Blazor Blazor WebAssembly Blazor
Server (WASM) Hybrid

Offloads processing to clients ❌ ✔️ ✔️

Full access to native client capabilities ❌ ❌ ✔️

Web-based deployment ✔️ ✔️ ❌

†Blazor WebAssembly and Blazor Hybrid apps can use server-based APIs to access
server/network resources and access private and secure app code.
‡Blazor WebAssembly only reaches near-native performance with ahead-of-time (AOT)
compilation.

After you choose the app's hosting model, you can generate a Blazor Server or Blazor
WebAssembly app from a Blazor project template. For more information, see Tooling for
ASP.NET Core Blazor.

To create a Blazor Hybrid app, see the articles under ASP.NET Core Blazor Hybrid
tutorials.

Complete .NET API compatibility


Blazor Server and Blazor Hybrid apps have complete .NET API compatibility, while Blazor
WebAssembly apps are limited to a subset of .NET APIs. When an app's specification
requires one or more .NET APIs that are unavailable to Blazor WebAssembly apps, then
choose Blazor Server or Blazor Hybrid.

Direct access to server and network resources


Blazor Server apps have direct access to server and network resources where the app is
executing. Because Blazor WebAssembly and Blazor Hybrid apps execute on a client,
they don't have direct access to server and network resources. Blazor WebAssembly and
Blazor Hybrid apps can access server and network resources indirectly via protected
server-based APIs. Server-based APIs might be available via third-party libraries,
packages, and services. Take into account the following considerations:

Third-party libraries, packages, and services might be costly to implement and


maintain, weakly supported, or introduce security risks.
If one or more server-based APIs are developed internally by your organization,
additional resources are required to build and maintain them.
To avoid server-based APIs for Blazor WebAssembly or Blazor Hybrid apps, adopt Blazor
Server, which can access server and network resources directly.

Small payload size with fast initial load time


Blazor Server apps have relatively small payload sizes with faster initial load times. When
a fast initial load time is desired, adopt Blazor Server.

Near native execution speed


Blazor Server apps generally execute on the server quickly. However, Blazor Server apps
are usually slower than other types of apps that execute natively on the client.

Blazor Hybrid apps run using the .NET runtime natively on the target platform, which
offers the best possible speed.

Blazor WebAssembly, including Progressive Web Apps (PWAs), apps run using the .NET
runtime for WebAssembly, which is slower than running directly on the platform, even
for apps that are ahead-of-time (AOT) compiled for WebAssembly in the browser.

App code secure and private on the server


Maintaining app code securely and privately on the server is a built-in feature of Blazor
Server. Blazor WebAssembly and Blazor Hybrid apps can use server-based APIs to access
functionality that must be kept private and secure. The considerations for developing
and maintaining server-based APIs described in the Direct access to server and network
resources section apply. If the development and maintenance of server-based APIs isn't
desirable for maintaining secure and private app code, adopt the Blazor Server hosting
model.

Run apps offline once downloaded


Blazor WebAssembly apps built as Progressive Web Apps (PWAs) and Blazor Hybrid
apps can run offline, which is particularly useful when clients aren't able to connect to
the Internet. Blazor Server apps fail to run when the connection to the server is lost. If an
app must run offline, Blazor WebAssembly and Blazor Hybrid are the best choices.

Static site hosting


Static site hosting is possible with Blazor WebAssembly apps because they're
downloaded to clients as a set of static files. Blazor WebAssembly apps don't require a
server to execute server-side code in order to download and run. Blazor WebAssembly
apps can be delivered via a Content Delivery Network (CDN) (for example, Azure
CDN ).

Although Blazor Hybrid apps are compiled into one or more self-contained deployment
assets, the assets are usually provided to clients through a third-party app store. If static
hosting is an app requirement, select Blazor WebAssembly.

Offloads processing to clients


Blazor WebAssembly and Blazor Hybrid apps execute on clients and thus offload
processing to clients. Blazor Server apps execute on a server, so server resource demand
typically increases with the number of users and the amount of processing required per
user. When it's possible to offload most or all of an app's processing to clients and the
app processes a significant amount of data, Blazor WebAssembly or Blazor Hybrid is the
best choice.

Full access to native client capabilities


Blazor Hybrid apps have full access to native client API capabilities via .NET native app
frameworks. In Blazor Hybrid apps, Razor components run directly in the native app, not
on WebAssembly . When full client capabilities are a requirement, Blazor Hybrid is the
best choice.

Web-based deployment
Blazor Server and Blazor WebAssembly are deployed as web apps that are updated on
the next app refresh.

Blazor Hybrid apps are native client apps that typically require an installer and platform-
specific deployment mechanism.

Additional resources
ASP.NET Core Blazor Hybrid
Tooling for ASP.NET Core Blazor
ASP.NET Core Blazor project structure
Overview of ASP.NET Core SignalR
ASP.NET Core Blazor SignalR guidance
Use ASP.NET Core SignalR with Blazor
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
Build a Blazor todo list app
Article • 05/30/2023

This tutorial provides a basic working experience for building and modifying a Blazor
app. For detailed Blazor guidance, see the Blazor reference documentation.

Learn how to:

" Create a todo list Blazor app project


" Modify Razor components
" Use event handling and data binding in components
" Use routing in a Blazor app

At the end of this tutorial, you'll have a working todo list app.

Prerequisites
Download and install .NET if it isn't already installed on the system or if the system
doesn't have the latest version installed.

Create a Blazor app


Create a new Blazor app named TodoList in a command shell:

.NET CLI

dotnet new blazorserver -o TodoList

The preceding command creates a folder named TodoList with the -o|--output option
to hold the app. The TodoList folder is the root folder of the project. Change directories
to the TodoList folder with the following command:

.NET CLI

cd TodoList

Build a todo list Blazor app


1. Add a new Todo Razor component to the app using the following command:
.NET CLI

dotnet new razorcomponent -n Todo -o Pages

The -n|--name option in the preceding command specifies the name of the new
Razor component. The new component is created in the project's Pages folder
with the -o|--output option.

) Important

Razor component file names require a capitalized first letter. Open the Pages
folder and confirm that the Todo component file name starts with a capital
letter T . The file name should be Todo.razor .

2. Open the Todo component in any file editor and add an @page Razor directive to
the top of the file with a relative URL of /todo .

Pages/Todo.razor :

razor

@page "/todo"

<PageTitle>Todo</PageTitle>

<h1>Todo</h1>

@code {

Save the Pages/Todo.razor file.

1. Add the Todo component to the navigation bar.

The NavMenu component is used in the app's layout. Layouts are components that
allow you to avoid duplication of content in an app. The NavLink component
provides a cue in the app's UI when the component URL is loaded by the app.

In the navigation element content ( <nav class="flex-column"> ) of the NavMenu


component, add the following <div> element for the Todo component.

In Shared/NavMenu.razor :
razor

<div class="nav-item px-3">


<NavLink class="nav-link" href="todo">
<span class="oi oi-list-rich" aria-hidden="true"></span> Todo
</NavLink>
</div>

Save the Shared/NavMenu.razor file.

1. Build and run the app by executing the dotnet watch run command in the
command shell from the TodoList folder. After the app is running, visit the new
Todo page by selecting the Todo link in the app's navigation bar, which loads the
page at /todo .

Leave the app running the command shell. Each time a file is saved, the app is
automatically rebuilt, and the page in the browser is automatically reloaded.

2. Add a TodoItem.cs file to the root of the project (the TodoList folder) to hold a
class that represents a todo item. Use the following C# code for the TodoItem class.

TodoItem.cs :

C#

public class TodoItem


{
public string? Title { get; set; }
public bool IsDone { get; set; }
}

7 Note

If using Visual Studio to create the TodoItem.cs file and TodoItem class, use either
of the following approaches:

Remove the namespace that Visual Studio generates for the class.
Use the Copy button in the preceding code block and replace the entire
contents of the file that Visual Studio generates.

1. Return to the Todo component and perform the following tasks:

Add a field for the todo items in the @code block. The Todo component uses
this field to maintain the state of the todo list.
Add unordered list markup and a foreach loop to render each todo item as a
list item ( <li> ).

Pages/Todo.razor :

razor

@page "/todo"

<PageTitle>Todo</PageTitle>

<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

@code {
private List<TodoItem> todos = new();
}

1. The app requires UI elements for adding todo items to the list. Add a text input
( <input> ) and a button ( <button> ) below the unordered list ( <ul>...</ul> ):

razor

@page "/todo"

<PageTitle>Todo</PageTitle>

<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

<input placeholder="Something todo" />


<button>Add todo</button>

@code {
private List<TodoItem> todos = new();
}
1. Save the TodoItem.cs file and the updated Pages/Todo.razor file. In the command
shell, the app is automatically rebuilt when the files are saved. The browser reloads
the page.

2. When the Add todo button is selected, nothing happens because an event handler
isn't attached to the button.

3. Add an AddTodo method to the Todo component and register the method for the
button using the @onclick attribute. The AddTodo C# method is called when the
button is selected:

razor

<input placeholder="Something todo" />


<button @onclick="AddTodo">Add todo</button>

@code {
private List<TodoItem> todos = new();

private void AddTodo()


{
// Todo: Add the todo
}
}

1. To get the title of the new todo item, add a newTodo string field at the top of the
@code block:

C#

private string? newTodo;

Modify the text <input> element to bind newTodo with the @bind attribute:

razor

<input placeholder="Something todo" @bind="newTodo" />

1. Update the AddTodo method to add the TodoItem with the specified title to the list.
Clear the value of the text input by setting newTodo to an empty string:

razor

@page "/todo"

<PageTitle>Todo</PageTitle>
<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

<input placeholder="Something todo" @bind="newTodo" />


<button @onclick="AddTodo">Add todo</button>

@code {
private List<TodoItem> todos = new();
private string? newTodo;

private void AddTodo()


{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}

1. Save the Pages/Todo.razor file. The app is automatically rebuilt in the command
shell, and the page reloads in the browser.

2. The title text for each todo item can be made editable, and a checkbox can help
the user keep track of completed items. Add a checkbox input for each todo item
and bind its value to the IsDone property. Change @todo.Title to an <input>
element bound to todo.Title with @bind :

razor

<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="todo.IsDone" />
<input @bind="todo.Title" />
</li>
}
</ul>

3. Update the <h1> header to show a count of the number of todo items that aren't
complete ( IsDone is false ). The Razor expression in the following header
evaluates each time Blazor rerenders the component.

razor

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

4. The completed Todo component ( Pages/Todo.razor ):

razor

@page "/todo"

<PageTitle>Todo</PageTitle>

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="todo.IsDone" />
<input @bind="todo.Title" />
</li>
}
</ul>

<input placeholder="Something todo" @bind="newTodo" />


<button @onclick="AddTodo">Add todo</button>

@code {
private List<TodoItem> todos = new();
private string? newTodo;

private void AddTodo()


{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}

1. Save the Pages/Todo.razor file. The app is automatically rebuilt in the command
shell, and the page reloads in the browser.

2. Add items, edit items, and mark todo items done to test the component.

3. When finished, shut down the app in the command shell. Many command shells
accept the keyboard command Ctrl + C (Windows) or ⌘ + C (macOS) to stop an
app.

Publish to Azure
For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.

Next steps
In this tutorial, you learned how to:

" Create a todo list Blazor app project


" Modify Razor components
" Use event handling and data binding in components
" Use routing in a Blazor app

Learn about tooling for ASP.NET Core Blazor:

ASP.NET Core Blazor


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)
ASP.NET Core Blazor Hybrid
Article • 04/04/2023

This article explains ASP.NET Core Blazor Hybrid, a way to build interactive client-side
web UI with .NET in an ASP.NET Core app.

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

In a Blazor Hybrid app, Razor components run natively on the device. Components
render to an embedded Web View control through a local interop channel. Components
don't run in the browser, and WebAssembly isn't involved. Razor components load and
execute code quickly, and components have full access to the native capabilities of the
device through the .NET platform. Component styles rendered in a Web View are
platform dependent and may require you to account for rendering differences across
platforms using custom stylesheets.

Blazor Hybrid articles cover subjects pertaining to integrating Razor components into
native client frameworks.

Blazor Hybrid apps with .NET MAUI


Blazor Hybrid support is built into the .NET Multi-platform App UI (.NET MAUI)
framework. .NET MAUI includes the BlazorWebView control that permits rendering Razor
components into an embedded Web View. By using .NET MAUI and Blazor together, you
can reuse one set of web UI components across mobile, desktop, and web.

Blazor Hybrid apps with WPF and Windows


Forms
Blazor Hybrid apps can be built with Windows Presentation Foundation (WPF) and
Windows Forms. Blazor provides BlazorWebView controls for both of these frameworks
(WPF BlazorWebView, Windows Forms BlazorWebView). Razor components run natively
in the Windows desktop and render to an embedded Web View. Using Blazor in WPF
and Windows Forms enables you to add new UI to your existing Windows desktop apps
that can be reused across platforms with .NET MAUI or on the web.

Web View configuration


Blazor Hybrid exposes the underlying Web View configuration for different platforms
through events of the BlazorWebView control:

BlazorWebViewInitializing provides access to the settings used to create the Web

View on each platform, if settings are available.


BlazorWebViewInitialized provides access to the Web View to allow further

configuration of the settings.

Use the preferred patterns on each platform to attach event handlers to the events to
execute your custom code.

API documentation:

.NET MAUI
BlazorWebViewInitializing
BlazorWebViewInitialized
WPF
BlazorWebViewInitializing
BlazorWebViewInitialized
Windows Forms
BlazorWebViewInitializing
BlazorWebViewInitialized

Unhandled exceptions in Windows Forms and


WPF apps
This section only applies to Windows Forms and WPF Blazor Hybrid apps.

Create a callback for UnhandledException on the System.AppDomain.CurrentDomain


property. The following example uses a compiler directive to display a MessageBox that
either alerts the user that an error has occurred or shows the error information to the
developer. Log the error information in error.ExceptionObject .

C#

AppDomain.CurrentDomain.UnhandledException += (sender, error) =>


{
#if DEBUG
MessageBox.Show(text: error.ExceptionObject.ToString(), caption:
"Error");
#else
MessageBox.Show(text: "An error has occurred.", caption: "Error");
#endif
// Log the error information (error.ExceptionObject)
};

Globalization and localization


This section only applies to .NET MAUI Blazor Hybrid apps.

.NET MAUI configures the CurrentCulture and CurrentUICulture based on the device's
ambient information.

IStringLocalizer and other API in the Microsoft.Extensions.Localization namespace


generally work as expected, along with globalization formatting, parsing, and binding
that relies on the user's culture.

When dynamically changing the app culture at runtime, the app must be reloaded to
reflect the change in culture, which takes care of rerendering the root component and
passing the new culture to rerendered child components.

.NET's resource system supports embedding localized images (as blobs) into an app, but
Blazor Hybrid can't display the embedded images in Razor components at this time.
Even if a user reads an image's bytes into a Stream using ResourceManager, the
framework doesn't currently support rendering the retrieved image in a Razor
component.

A platform-specific approach to include localized images is a feature of .NET's resource


system, but a Razor component's browser elements in a .NET MAUI Blazor Hybrid app
aren't able to interact with such images.

For more information, see the following resources:

Xamarin.Forms String and Image Localization: The guidance generally applies to


Blazor Hybrid apps. Not every scenario is supported at this time.
Blazor Image component to display images that are not accessible through HTTP
endpoints (dotnet/aspnetcore #25274)

Additional resources
ASP.NET Core Blazor Hybrid tutorials
.NET Multi-platform App UI (.NET MAUI)
Windows Presentation Foundation (WPF)
Windows Forms
ASP.NET Core Blazor Hybrid tutorials
Article • 05/30/2023

The following tutorials provide a basic working experience for building a Blazor Hybrid
app:

Build a .NET MAUI Blazor Hybrid app

Build a Windows Forms Blazor app

Build a Windows Presentation Foundation (WPF) Blazor app

For an overview of Blazor and reference articles, see ASP.NET Core Blazor and the
articles that follow it in the table of contents.
Build a .NET MAUI Blazor Hybrid app
Article • 04/04/2023

This tutorial shows you how to build and run a .NET MAUI Blazor Hybrid app. You learn
how to:

" Create a .NET MAUI Blazor Hybrid app project in Visual Studio


" Run the app on Windows
" Run the app on an emulated mobile device in the Android Emulator

Prerequisites
Supported platforms (.NET MAUI documentation)
Visual Studio with the .NET Multi-platform App UI development workload.
Microsoft Edge WebView2: WebView2 is required on Windows when running a
native app. When developing .NET MAUI Blazor Hybrid apps and only running
them in Visual Studio's emulators, WebView2 isn't required.
Enable hardware acceleration to improve the performance of the Android
emulator.

For more information on prerequisites and installing software for this tutorial, see the
following resources in the .NET MAUI documentation:

Supported platforms for .NET MAUI apps


Installation (Visual Studio)

Create a .NET MAUI Blazor Hybrid app


Launch Visual Studio. In the Start Window, select Create a new project:
In the Create a new project window, use the Project type dropdown to filter MAUI
templates:

Select the .NET MAUI Blazor Hybrid App template and then select the Next button:
7 Note

In .NET 7.0 or earlier, the template is named .NET MAUI Blazor App.

In the Configure your new project dialog:

Set the Project name to MauiBlazor.


Choose a suitable location for the project.
Select the Next button.
In the Additional information dialog, select the framework version with the Framework
dropdown list. Select the Create button:

Wait for Visual Studio to create the project and restore the project's dependencies.
Monitor the progress in Solution Explorer by opening the Dependencies entry.
Dependencies restoring:

Dependencies restored:

Run the app on Windows


In the Visual Studio toolbar, select the Windows Machine button to build and start the
project:

If Developer Mode isn't enabled, you're prompted to enable it in Settings > For
developers > Developer Mode (Windows 10) or Settings > Privacy & security > For
developers > Developer Mode (Windows 11). Set the switch to On.

The app running as a Windows desktop app:


Run the app in the Android Emulator
If you followed the guidance in the Run the app on Windows section, select the Stop
Debugging button in the toolbar to stop the running Windows app:

In the Visual Studio toolbar, select the start configuration dropdown button. Select
Android Emulators > Android Emulator:

Android SDKs are required to build apps for Android. In the Error List panel, a message
appears asking you to double-click the message to install the required Android SDKs:

The Android SDK License Acceptance window appears, select the Accept button for
each license that appears. An additional window appears for the Android Emulator and
SDK Patch Applier licenses. Select the Accept button.

Wait for Visual Studio to download the Android SDK and Android Emulator. You can
track the progress by selecting the background tasks indicator in the lower-left corner of
the Visual Studio UI:

The indicator shows a checkmark when the background tasks are complete:
In the toolbar, select the Android Emulator button:

In the Create a Default Android Device window, select the Create button:

Wait for Visual Studio to download, unzip, and create an Android Emulator. When the
Android phone emulator is ready, select the Start button.

7 Note

Enable hardware acceleration to improve the performance of the Android


emulator.

Close the Android Device Manager window. Wait until the emulated phone window
appears, the Android OS loads, and the home screen appears.

) Important

The emulated phone must be powered on with the Android OS loaded in order to
load and run the app in the debugger. If the emulated phone isn't running, turn on
the phone using either the Ctrl + P keyboard shortcut or by selecting the Power
button in the UI:

In the Visual Studio toolbar, select the Pixel 5 - {VERSION} button to build and run the
project, where the {VERSION} placeholder is the Android version. In the following
example, the Android version is API 30 (Android 11.0 - API 30), and a later version
appears depending on the Android SDK installed:

Visual Studio builds the project and deploys the app to the emulator.

Starting the emulator, loading the emulated phone and OS, and deploying and running
the app can take several minutes depending on the speed of the system and whether or
not hardware acceleration is enabled. You can monitor the progress of the deployment
by inspecting Visual Studio's status bar at the bottom of the UI. The Ready indicator
receives a checkmark and the emulator's deployment and app loading indicators
disappear when the app is running:

During deployment:

During app startup:

The app running in the Android Emulator:


Next steps
In this tutorial, you learned how to:

" Create a .NET MAUI Blazor Hybrid app project in Visual Studio


" Run the app on Windows
" Run the app on an emulated mobile device in the Android Emulator

Learn more about Blazor Hybrid apps:

ASP.NET Core Blazor Hybrid


Build a Windows Forms Blazor app
Article • 04/04/2023

This tutorial shows you how to build and run a Windows Forms Blazor app. You learn
how to:

" Create a Windows Forms Blazor app project


" Run the app on Windows

Prerequisites
Supported platforms (Windows Forms documentation)
Visual Studio 2022 with the .NET desktop development workload

Visual Studio workload


If the .NET desktop development workload isn't installed, use the Visual Studio installer
to install the workload. For more information, see Modify Visual Studio workloads,
components, and language packs.

Create a Windows Forms Blazor project


Launch Visual Studio. In the Start Window, select Create a new project:
In the Create a new project dialog, filter the Project type dropdown to Desktop. Select
the C# project template for Windows Forms App and select the Next button:

In the Configure your new project dialog:

Set the Project name to WinFormsBlazor.


Choose a suitable location for the project.
Select the Next button.

In the Additional information dialog, select the framework version with the Framework
dropdown list. Select the Create button:

Use NuGet Package Manager to install the


Microsoft.AspNetCore.Components.WebView.WindowsForms NuGet package:
In Solution Explorer, right-click the project's name, WinFormsBlazor, and select Edit
Project File to open the project file ( WinFormsBlazor.csproj ).

At the top of the project file, change the SDK to Microsoft.NET.Sdk.Razor :

XML

<Project Sdk="Microsoft.NET.Sdk.Razor">

Save the changes to the project file ( WinFormsBlazor.csproj ).

Add an _Imports.razor file to the root of the project with an @using directive for
Microsoft.AspNetCore.Components.Web.

_Imports.razor :

razor

@using Microsoft.AspNetCore.Components.Web

Save the _Imports.razor file.

Add a wwwroot folder to the project.

Add an index.html file to the wwwroot folder with the following markup.

wwwroot/index.html :

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WinFormsBlazor</title>
<base href="/" />
<link href="css/app.css" rel="stylesheet" />
<link href="WinFormsBlazor.styles.css" rel="stylesheet" />
</head>

<body>

<div id="app">Loading...</div>

<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

<script src="_framework/blazor.webview.js"></script>

</body>

</html>

Inside the wwwroot folder, create a css folder to hold stylesheets.

Add an app.css stylesheet to the wwwroot/css folder with the following content.

wwwroot/css/app.css :

css

html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1:focus {
outline: none;
}

a, .btn-link {
color: #0071c1;
}

.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}

.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}

.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}

#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}

#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

Add the following Counter component to the root of the project, which is the default
Counter component found in Blazor project templates.

Counter.razor :

razor

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

Save the Counter component ( Counter.razor ).

In Solution Explorer, double-click on the Form1.cs file to open the designer:


Open the Toolbox by either selecting the Toolbox button along the left edge of the
Visual Studio window or selecting the View > Toolbox menu command.

Locate the BlazorWebView control under


Microsoft.AspNetCore.Components.WebView.WindowsForms . Drag the BlazorWebView from

the Toolbox into the Form1 designer. Be careful not to accidentally drag a WebView2
control into the form.

Visual Studio shows the BlazorWebView control in the form designer as WebView2 and
automatically names the control blazorWebView1 :
In Form1 , select the BlazorWebView ( WebView2 ) with a single click.

In the BlazorWebView's Properties, confirm that the control is named blazorWebView1 . If


the name isn't blazorWebView1 , the wrong control was dragged from the Toolbox.
Delete the WebView2 control in Form1 and drag the BlazorWebView control into the form.

In the control's properties, change the BlazorWebView's Dock value to Fill:

In the Form1 designer, right-click Form1 and select View Code.

Add namespaces for Microsoft.AspNetCore.Components.WebView.WindowsForms and


Microsoft.Extensions.DependencyInjection to the top of the Form1.cs file:

C#

using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;
Inside the Form1 constructor, after the InitializeComponent method call, add the
following code:

C#

var services = new ServiceCollection();


services.AddWindowsFormsBlazorWebView();
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
blazorWebView1.RootComponents.Add<Counter>("#app");

7 Note

The InitializeComponent method is generated by a source generator at app build


time and added to the compilation object for the calling class.

The final, complete C# code of Form1.cs with a file-scoped namespace:

C#

using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;

namespace WinFormsBlazor;

public partial class Form1 : Form


{
public Form1()
{
InitializeComponent();

var services = new ServiceCollection();


services.AddWindowsFormsBlazorWebView();
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
blazorWebView1.RootComponents.Add<Counter>("#app");
}
}

Run the app


Select the start button in the Visual Studio toolbar:

The app running on Windows:


Next steps
In this tutorial, you learned how to:

" Create a Windows Forms Blazor app project


" Run the app on Windows

Learn more about Blazor Hybrid apps:

ASP.NET Core Blazor Hybrid


Build a Windows Presentation
Foundation (WPF) Blazor app
Article • 04/04/2023

This tutorial shows you how to build and run a WPF Blazor app. You learn how to:

" Create a WPF Blazor app project


" Add a Razor component to the project
" Run the app on Windows

Prerequisites
Supported platforms (WPF documentation)
Visual Studio 2022 with the .NET desktop development workload

Visual Studio workload


If the .NET desktop development workload isn't installed, use the Visual Studio installer
to install the workload. For more information, see Modify Visual Studio workloads,
components, and language packs.

Create a WPF Blazor project


Launch Visual Studio. In the Start Window, select Create a new project:
In the Create a new project dialog, filter the Project type dropdown to Desktop. Select
the C# project template for WPF Application and select the Next button:

In the Configure your new project dialog:

Set the Project name to WpfBlazor.


Choose a suitable location for the project.
Select the Next button.

In the Additional information dialog, select the framework version with the Framework
dropdown list. Select the Create button:

Use NuGet Package Manager to install the


Microsoft.AspNetCore.Components.WebView.Wpf NuGet package:
In Solution Explorer, right-click the project's name, WpfBlazor, and select Edit Project
File to open the project file ( WpfBlazor.csproj ).

At the top of the project file, change the SDK to Microsoft.NET.Sdk.Razor :

XML

<Project Sdk="Microsoft.NET.Sdk.Razor">

In the project file's existing <PropertyGroup> add the following markup to set the app's
root namespace, which is WpfBlazor in this tutorial:

XML

<RootNamespace>WpfBlazor</RootNamespace>

7 Note

The preceding guidance on setting the project's root namespace is a temporary


workaround. For more information, see [Blazor][Wpf] Root namespace related
issue (dotnet/maui #5861) .

Save the changes to the project file ( WpfBlazor.csproj ).

Add an _Imports.razor file to the root of the project with an @using directive for
Microsoft.AspNetCore.Components.Web.

_Imports.razor :

razor

@using Microsoft.AspNetCore.Components.Web
Save the _Imports.razor file.

Add a wwwroot folder to the project.

Add an index.html file to the wwwroot folder with the following markup.

wwwroot/index.html :

HTML

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WpfBlazor</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="WpfBlazor.styles.css" rel="stylesheet" />
</head>

<body>
<div id="app">Loading...</div>

<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webview.js"></script>
</body>

</html>

Inside the wwwroot folder, create a css folder.

Add an app.css stylesheet to the wwwroot/css folder with the following content.

wwwroot/css/app.css :

css

html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}

.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}

.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}

.invalid {
outline: 1px solid red;
}

.validation-message {
color: red;
}

#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}

#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

Add the following Counter component to the root of the project, which is the default
Counter component found in Blazor project templates.

Counter.razor :

razor

<h1>Counter</h1>

<p>Current count: @currentCount</p>


<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

Save the Counter component ( Counter.razor ).

If the MainWindow designer isn't open, open it by double-clicking the MainWindow.xaml


file in Solution Explorer. In the MainWindow designer, replace the XAML code with the
following:

XAML

<Window x:Class="WpfBlazor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-
compatibility/2006"
xmlns:blazor="clr-
namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.Asp
NetCore.Components.WebView.Wpf"
xmlns:local="clr-namespace:WpfBlazor"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<blazor:BlazorWebView HostPage="wwwroot\index.html" Services="
{DynamicResource services}">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app" ComponentType="{x:Type
local:Counter}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
</Grid>
</Window>

In Solution Explorer, right-click MainWindow.xaml and select View Code:


Add the namespace Microsoft.Extensions.DependencyInjection to the top of the
MainWindow.xaml.cs file:

C#

using Microsoft.Extensions.DependencyInjection;

Inside the MainWindow constructor, after the InitializeComponent method call, add the
following code:

C#

var serviceCollection = new ServiceCollection();


serviceCollection.AddWpfBlazorWebView();
Resources.Add("services", serviceCollection.BuildServiceProvider());

7 Note

The InitializeComponent method is generated by a source generator at app build


time and added to the compilation object for the calling class.

The final, complete C# code of MainWindow.xaml.cs with a file-scoped namespace and


comments removed:

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Extensions.DependencyInjection;

namespace WpfBlazor;

public partial class MainWindow : Window


{
public MainWindow()
{
InitializeComponent();

var serviceCollection = new ServiceCollection();


serviceCollection.AddWpfBlazorWebView();
Resources.Add("services", serviceCollection.BuildServiceProvider());
}
}

Run the app


Select the start button in the Visual Studio toolbar:

The app running on Windows:


Next steps
In this tutorial, you learned how to:

" Create a WPF Blazor app project


" Add a Razor component to the project
" Run the app on Windows

Learn more about Blazor Hybrid apps:

ASP.NET Core Blazor Hybrid


ASP.NET Core Blazor Hybrid routing and
navigation
Article • 04/18/2023

This article explains how to manage request routing and navigation in Blazor Hybrid
apps.

URI request routing behavior


Default URI request routing behavior:

A link is internal if the host name and scheme match between the app's origin URI
and the request URI. When the host names and schemes don't match or if the link
sets target="_blank" , the link is considered external.
If the link is internal, the link is opened in the BlazorWebView by the app.
If the link is external, the link is opened by an app determined by the device based
on the device's registered handler for the link's scheme.
For internal links that appear to request a file because the last segment of the URI
uses dot notation (for example, /file.x , /Maryia.Melnyk , /image.gif ) but don't
point to any static content:
WPF and Windows Forms: The host page content is returned.
.NET MAUI: A 404 response is returned.

To change the link handling behavior for links that don't set target="_blank" , register
the UrlLoading event and set the UrlLoadingEventArgs.UrlLoadingStrategy property. The
UrlLoadingStrategy enumeration allows setting link handling behavior to any of the
following values:

OpenExternally: Load the URL using an app determined by the device. This is the
default strategy for URIs with an external host.
OpenInWebView: Load the URL within the BlazorWebView . This is the default
strategy for URLs with a host matching the app origin. Don't use this strategy for
external links unless you can ensure the destination URI is fully trusted.
CancelLoad: Cancels the current URL loading attempt.

The UrlLoadingEventArgs.Url property is used to get or dynamically set the URL.

2 Warning
By default, external links are opened in an app determined by the device. Opening
external links within a BlazorWebView can introduce security vulnerabilities and
should not be enabled unless you can ensure that the external links are fully
trusted.

API documentation:

.NET MAUI: BlazorWebView.UrlLoading


WPF: BlazorWebView.UrlLoading
Windows Forms: BlazorWebView.UrlLoading

The Microsoft.AspNetCore.Components.WebView namespace is required for the


following examples:

C#

using Microsoft.AspNetCore.Components.WebView;

Add the following event handler to the constructor of the Page where the
BlazorWebView is created, which is MainPage.xaml.cs in an app created from the .NET
MAUI project template.

C#

blazorWebView.UrlLoading +=
(sender, urlLoadingEventArgs) =>
{
if (urlLoadingEventArgs.Url.Host != "0.0.0.0")
{
urlLoadingEventArgs.UrlLoadingStrategy =
UrlLoadingStrategy.OpenInWebView;
}
};

Navigation among pages and Razor


components
This section explains how to navigate among .NET MAUI content pages and Razor
components.

The .NET MAUI Blazor hybrid project template isn't a Shell-based app, so the URI-based
navigation for Shell-based apps isn't suitable for a project based on the project
template. The examples in this section use a NavigationPage to perform modeless or
modal navigation.

In the following example:

The namespace of the app is MauiBlazor , which matches the suggested project
name of the Build a .NET MAUI Blazor Hybrid app tutorial.
A ContentPage is placed in a new folder added to the app named Views .

In App.xaml.cs , create the MainPage as a NavigationPage by making the following


change:

diff

- MainPage = new MainPage();


+ MainPage = new NavigationPage(new MainPage());

Views/NavigationExample.xaml :

XAML

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MauiBlazor"
x:Class="MauiBlazor.Views.NavigationExample"
Title="Navigation Example"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<StackLayout>
<Label Text="Navigation Example"
VerticalOptions="Center"
HorizontalOptions="Center"
FontSize="24" />
<Button x:Name="CloseButton"
Clicked="CloseButton_Clicked"
Text="Close" />
</StackLayout>
</ContentPage>

In the following NavigationExample code file, the CloseButton_Clicked event handler for
the close button calls PopAsync to pop the ContentPage off of the navigation stack.

Views/NavigationExample.xaml.cs :

C#

namespace MauiBlazor.Views;

public partial class NavigationExample : ContentPage


{
public NavigationExample()
{
InitializeComponent();
}

private async void CloseButton_Clicked(object sender, EventArgs e)


{
await Navigation.PopAsync();
}
}

In a Razor component:

Add the namespace for the app's content pages. In the following example, the
namespace is MauiBlazor.Views .
Add an HTML button element with an @onclick event handler to open the content
page. The event handler method is named OpenPage .
In the event handler, call PushAsync to push the ContentPage, NavigationExample ,
onto the navigation stack.

The following example is based on the Index component in the .NET MAUI Blazor
project template.

Pages/Index.razor :

razor

@page "/"
@using MauiBlazor.Views

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<button class="btn btn-primary" @onclick="OpenPage">Open</button>

@code {
private async void OpenPage()
{
await App.Current.MainPage.Navigation.PushAsync(new
NavigationExample());
}
}

To change the preceding example to modal navigation:


In the CloseButton_Clicked method ( Views/NavigationExample.xaml.cs ), change
PopAsync to PopModalAsync:

diff

- await Navigation.PopAsync();
+ await Navigation.PopModalAsync();

In the OpenPage method ( Pages/Index.razor ), change PushAsync to


PushModalAsync:

diff

- await App.Current.MainPage.Navigation.PushAsync(new
NavigationExample());
+ await App.Current.MainPage.Navigation.PushModalAsync(new
NavigationExample());

For more information, see the following resources:

NavigationPage article (.NET MAUI documentation)


NavigationPage (API documentation)
ASP.NET Core Blazor Hybrid static files
Article • 04/25/2023

This article describes how to consume static asset files in Blazor Hybrid apps.

In a Blazor Hybrid app, static files are app resources, accessed by Razor components
using the following approaches:

.NET MAUI: .NET MAUI file system helpers


WPF and Windows Forms: ResourceManager

When static assets are only used in the Razor components, static assets can be
consumed from the web root ( wwwroot folder) in a similar way to Blazor WebAssembly
and Blazor Server apps. For more information, see the Static assets limited to Razor
components section.

.NET MAUI
In .NET MAUI apps, raw assets using the MauiAsset build action and .NET MAUI file
system helpers are used for static assets.

7 Note

Interfaces, classes, and supporting types to work with storage on devices across all
supported platforms for features such as choosing a file, saving preferences, and
using secure storage are in the Microsoft.Maui.Storage namespace. The
namespace is available throughout a MAUI Blazor Hybrid app, so there's no need
to specify a using statement in a class file or an @using Razor directive in a Razor
component for the namespace.

Place raw assets into the Resources/Raw folder of the app. The example in this section
uses a static text file.

Resources/Raw/Data.txt :

text

This is text from a static text file resource.

The following Razor component:


Calls OpenAppPackageFileAsync to obtain a Stream for the resource.
Reads the Stream with a StreamReader.
Calls StreamReader.ReadToEndAsync to read the file.

Pages/StaticAssetExample.razor :

razor

@page "/static-asset-example"
@using System.IO
@using Microsoft.Extensions.Logging
@inject ILogger<StaticAssetExample> Logger

<h1>Static Asset Example</h1>

<p>@dataResourceText</p>

@code {
public string dataResourceText = "Loading resource ...";

protected override async Task OnInitializedAsync()


{
try
{
using var stream =
await FileSystem.OpenAppPackageFileAsync("Data.txt");
using var reader = new StreamReader(stream);

dataResourceText = await reader.ReadToEndAsync();


}
catch (FileNotFoundException ex)
{
dataResourceText = "Data file not found.";
Logger.LogError(ex, "'Resource/Raw/Data.txt' not found.");
}
}
}

For more information, see the following resources:

Target multiple platforms from .NET MAUI single project (.NET MAUI
documentation)
Improve consistency with resizetizer (dotnet/maui #4367)

WPF
Place the asset into a folder of the app, typically at the project's root, such as a
Resources folder. The example in this section uses a static text file.
Resources/Data.txt :

text

This is text from a static text file resource.

If a Properties folder doesn't exist in the app, create a Properties folder in the root of
the app.

If the Properties folder doesn't contain a resources file ( Resources.resx ), create the file
in Solution Explorer with the Add > New Item contextual menu command.

Double-click the Resource.resx file.

Select Strings > Files from the dropdown list.

Select Add Resource > Add Existing File. If prompted by Visual Studio to confirm
editing the file, select Yes. Navigate to the Resources folder, select the Data.txt file, and
select Open.

In the following example component, ResourceManager.GetString obtains the string


resource's text for display.

2 Warning

Never use ResourceManager methods with untrusted data.

StaticAssetExample.razor :

razor

@page "/static-asset-example"
@using System.Resources

<h1>Static Asset Example</h1>

<p>@dataResourceText</p>

@code {
public string dataResourceText = "Loading resource ...";

protected override void OnInitialized()


{
var resources =
new ResourceManager(typeof(WpfBlazor.Properties.Resources));

dataResourceText = resources.GetString("Data") ?? "'Data' not


found.";
}
}

Windows Forms
Place the asset into a folder of the app, typically at the project's root, such as a
Resources folder. The example in this section uses a static text file.

Resources/Data.txt :

text

This is text from a static text file resource.

Examine the files associated with Form1 in Solution Explorer. If Form1 doesn't have a
resource file ( .resx ), add a Form1.resx file with the Add > New Item contextual menu
command.

Double-click the Form1.resx file.

Select Strings > Files from the dropdown list.

Select Add Resource > Add Existing File. If prompted by Visual Studio to confirm
editing the file, select Yes. Navigate to the Resources folder, select the Data.txt file, and
select Open.

In the following example component:

The app's assembly name is WinFormsBlazor . The ResourceManager's base name is


set to the assembly name of Form1 ( WinFormsBlazor.Form1 ).
ResourceManager.GetString obtains the string resource's text for display.

2 Warning

Never use ResourceManager methods with untrusted data.

StaticAssetExample.razor :

razor

@page "/static-asset-example"
@using System.Resources
<h1>Static Asset Example</h1>

<p>@dataResourceText</p>

@code {
public string dataResourceText = "Loading resource ...";

protected override async Task OnInitializedAsync()


{
var resources =
new ResourceManager("WinFormsBlazor.Form1",
this.GetType().Assembly);

dataResourceText = resources.GetString("Data") ?? "'Data' not


found.";
}
}

Static assets limited to Razor components


A BlazorWebView control has a configured host file (HostPage), typically
wwwroot/index.html . The HostPage path is relative to the project. All static web assets
(scripts, CSS files, images, and other files) that are referenced from a BlazorWebView are
relative to its configured HostPage.

Static web assets from a Razor class library (RCL) use special paths: _content/{PACKAGE
ID}/{PATH AND FILE NAME} . The {PACKAGE ID} placeholder is the library's package ID. The

package ID defaults to the project's assembly name if <PackageId> isn't specified in the
project file. The {PATH AND FILE NAME} placeholder is path and file name under wwwroot .
These paths are logically subpaths of the app's wwwroot folder, although they're actually
coming from other packages or projects. Component-specific CSS style bundles are also
built at the root of the wwwroot folder.

The web root of the HostPage determines which subset of static assets are available:

wwwroot/index.html (Recommended): All assets in the app's wwwroot folder are

available (for example: wwwroot/image.png is available from /image.png ), including


subfolders (for example: wwwroot/subfolder/image.png is available from
/subfolder/image.png ). RCL static assets in the RCL's wwwroot folder are available

(for example: wwwroot/image.png is available from the path _content/{PACKAGE


ID}/image.png ), including subfolders (for example: wwwroot/subfolder/image.png is

available from the path _content/{PACKAGE ID}/subfolder/image.png ).


wwwroot/{PATH}/index.html : All assets in the app's wwwroot/{PATH} folder are

available using app web root relative paths. RCL static assets in wwwroot/{PATH} are
not available because they would be in a non-existent theoretical location, such as
../../_content/{PACKAGE ID}/{PATH} , which is not a supported relative path.
wwwroot/_content/{PACKAGE ID}/index.html : All assets in the RCL's wwwroot/{PATH}

folder are available using RCL web root relative paths. The app's static assets in
wwwroot/{PATH} are not available because they would be in a non-existent
theoretical location, such as ../../{PATH} , which is not a supported relative path.

For most apps, we recommend placing the HostPage at the root of the wwwroot folder
of the app, which provides the greatest flexibility for supplying static assets from the
app, RCLs, and via subfolders of the app and RCLs.

The following examples demonstrate referencing static assets from the app's web root
( wwwroot folder) with a HostPage rooted in the wwwroot folder.

wwwroot/data.txt :

text

This is text from a static text file resource.

wwwroot/scripts.js :

JavaScript

export function showPrompt(message) {


return prompt(message, 'Type anything here');
}

The following Jeep® image is also used in this section's example. You can right-click the
following image to save it locally for use in a local test app.

wwwroot/jeep-yj.png :
In a Razor component:

The static text file contents can be read using the following techniques:
.NET MAUI: .NET MAUI file system helpers (OpenAppPackageFileAsync)
WPF and Windows Forms: StreamReader.ReadToEndAsync
JavaScript files are available at logical subpaths of wwwroot using ./ paths.
The image can be the source attribute ( src ) of an image tag ( <img> ).

StaticAssetExample2.razor :

razor

@page "/static-asset-example-2"
@using Microsoft.Extensions.Logging
@implements IAsyncDisposable
@inject IJSRuntime JS
@inject ILogger<StaticAssetExample2> Logger

<h1>Static Asset Example 2</h1>

<h2>Read a file</h2>

<p>@dataResourceText</p>

<h2>Call JavaScript</h2>

<p>
<button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>@result</p>

<h2>Show an image</h2>

<p><img alt="1991 Jeep YJ" src="/jeep-yj.png" /></p>

<p>
<em>Jeep</em> and <em>Jeep YJ</em> are registered trademarks of
<a href="https://www.stellantis.com">FCA US LLC (Stellantis NV)</a>.
</p>

@code {
private string dataResourceText = "Loading resource ...";
private IJSObjectReference module;
private string result;

protected override async Task OnInitializedAsync()


{
try
{
dataResourceText = await ReadData();
}
catch (FileNotFoundException ex)
{
dataResourceText = "Data file not found.";
Logger.LogError(ex, "'wwwroot/data.txt' not found.");
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>("import",
"./scripts.js");
}
}

private async Task TriggerPrompt()


{
result = await Prompt("Provide some text");
}

public async ValueTask<string> Prompt(string message) =>


module is not null ?
await module.InvokeAsync<string>("showPrompt", message) : null;

async ValueTask IAsyncDisposable.DisposeAsync()


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

In .NET MAUI apps, add the following ReadData method to the @code block of the
preceding component:

C#
private async Task<string> ReadData()
{
using var stream = await
FileSystem.OpenAppPackageFileAsync("wwwroot/data.txt");
using var reader = new StreamReader(stream);

return await reader.ReadToEndAsync();


}

In WPF and Windows Forms apps, add the following ReadData method to the @code
block of the preceding component:

C#

private async Task<string> ReadData()


{
using var reader = new StreamReader("wwwroot/data.txt");

return await reader.ReadToEndAsync();


}

Collocated JavaScript files are also accessible at logical subpaths of wwwroot . Instead of
using the script described earlier for the showPrompt function in wwwroot/scripts.js , the
following collocated JavaScript file for the StaticAssetExample2 component also makes
the function available.

Pages/StaticAssetExample2.razor.js :

JavaScript

export function showPrompt(message) {


return prompt(message, 'Type anything here');
}

Modify the module object reference in the StaticAssetExample2 component to use the
collocated JavaScript file path ( ./Pages/StaticAssetExample2.razor.js ):

C#

module = await JS.InvokeAsync<IJSObjectReference>("import",


"./Pages/StaticAssetExample2.razor.js");

Trademarks
Jeep and Jeep YJ are registered trademarks of FCA US LLC (Stellantis NV) .

Additional resources
ResourceManager
Create resource files for .NET apps (.NET Fundamentals documentation)
How to: Use resources in localizable apps (WPF documentation)
Use browser developer tools with
ASP.NET Core Blazor Hybrid
Article • 04/04/2023

This article explains how to use browser developer tools with Blazor Hybrid apps.

Browser developer tools with .NET MAUI Blazor


Ensure the Blazor Hybrid project is configured to support browser developer tools. You
can confirm developer tools support by searching the app for
AddBlazorWebViewDeveloperTools .

If the project isn't already configured for browser developer tools, add support by:

1. Locating where the call to AddMauiBlazorWebView is made, likely within the app's
MauiProgram.cs file.

2. At the top of the MauiProgram.cs file, confirm the presence of a using statement
for Microsoft.Extensions.Logging. If the using statement isn't present, add it to the
top of the file:

C#

using Microsoft.Extensions.Logging;

3. After the call to AddMauiBlazorWebView, add the following code:

C#

#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif

To use browser developer tools with a Windows app:

1. Run the .NET MAUI Blazor Hybrid app for Windows and navigate to an app page
that uses a BlazorWebView. The developer tools console is unavailable from
ContentPages without a Blazor Web View.

2. Use the keyboard shortcut Ctrl + Shift + I to open browser developer tools.
3. Developer tools provide a variety of features for working with apps, including
which assets the page requested, how long assets took to load, and the content of
loaded assets. The following example shows the Console tab to see the console
messages, which includes any exception messages generated by the framework or
developer code:

Additional resources
Chrome DevTools
Microsoft Edge Developer Tools overview
Safari Developer Help
Reuse Razor components in ASP.NET
Core Blazor Hybrid
Article • 04/04/2023

This article explains how to author and organize Razor components for the web and
Web Views in Blazor Hybrid apps.

Razor components work across hosting models (Blazor WebAssembly, Blazor Server, and
in the Web View of Blazor Hybrid) and across platforms (Android, iOS, and Windows).
Hosting models and platforms have unique capabilities that components can leverage,
but components executing across hosting models and platforms must leverage unique
capabilities separately, which the following examples demonstrate:

Blazor WebAssembly supports synchronous JavaScript (JS) interop, which isn't


supported by the strictly asynchronous JS interop communication channel in
Blazor Server and Web Views of Blazor Hybrid apps.
Components in a Blazor Server app can access services that are only available on
the server, such as an Entity Framework database context.
Components in a BlazorWebView can directly access native desktop and mobile
device features, such as geolocation services. Blazor Server and Blazor
WebAssembly apps must rely upon web API interfaces of apps on external servers
to provide similar features.

Design principles
In order to author Razor components that can seamlessly work across hosting models
and platforms, adhere to the following design principles:

Place shared UI code in Razor class libraries (RCLs), which are containers designed
to maintain reusable pieces of UI for use across different hosting models and
platforms.
Implementations of unique features shouldn't exist in RCLs. Instead, the RCL
should define abstractions (interfaces and base classes) that hosting models and
platforms implement.
Only opt-in to unique features by hosting model or platform. For example, Blazor
WebAssembly supports the use of IJSInProcessRuntime and
IJSInProcessObjectReference in a component as an optimization, but only use
them with conditional casts and fallback implementations that rely on the universal
IJSRuntime and IJSObjectReference abstractions that all hosting models and
platforms support. For more information on IJSInProcessRuntime, see Call
JavaScript functions from .NET methods in ASP.NET Core Blazor. For more
information on IJSInProcessObjectReference, see Call .NET methods from
JavaScript functions in ASP.NET Core Blazor.
As a general rule, use CSS for HTML styling in components. The most common
case is for consistency in the look and feel of an app. In places where UI styles
must differ across hosting models or platforms, use CSS to style the differences.
If some part of the UI requires additional or different content for a target hosting
model or platform, the content can be encapsulated inside a component and
rendered inside the RCL using DynamicComponent. Additional UI can also be
provided to components via RenderFragment instances. For more information on
RenderFragment, see Child content render fragments and Render fragments for
reusable rendering logic.

Project code organization


As much as possible, place code and static content in Razor class libraries (RCLs). Each
hosting model or platform references the RCL and registers individual implementations
in the app's service collection that a Razor component might require.

Each target assembly should contain only the code that is specific to that hosting model
or platform along with the code that helps bootstrap the app.

Use abstractions for unique features


The following example demonstrates how to use an abstraction for a geolocation
service by hosting model and platform.

In a Razor class library (RCL) used by the app to obtain geolocation data for the
user's location on a map, the MapComponent Razor component injects an
ILocationService service abstraction.

App.Web for Blazor WebAssembly and Blazor Server projects implement

ILocationService as WebLocationService , which uses web API calls to obtain


geolocation data.
App.Desktop for .NET MAUI, WPF, and Windows Forms, implement
ILocationService as DesktopLocationService . DesktopLocationService uses

platform-specific device features to obtain geolocation data.

.NET MAUI Blazor platform-specific code


A common pattern in .NET MAUI is to create separate implementations for different
platforms, such as defining partial classes with platform-specific implementations. For
example, see the following diagram, where partial classes for CameraService are
implemented in each of CameraService.Windows.cs , CameraService.iOS.cs ,
CameraService.Android.cs , and CameraService.cs :
Where you want to pack platform-specific features into a class library that can be
consumed by other apps, we recommend that you follow a similar approach to the one
described in the preceding example and create an abstraction for the Razor component:

Place the component in a Razor class library (RCL).


From a .NET MAUI class library, reference the RCL and create the platform-specific
implementations.
Within the consuming app, reference the .NET MAUI class library.

The following example demonstrates the concepts for images in an app that organizes
photographs:

A .NET MAUI Blazor Hybrid app uses InputPhoto from an RCL that it references.
The .NET MAUI app also references a .NET MAUI class library.
InputPhoto in the RCL injects an ICameraService interface, which is defined in the
RCL.
CameraService partial class implementations for ICameraService are in the .NET
MAUI class library ( CameraService.Windows.cs , CameraService.iOS.cs ,
CameraService.Android.cs ), which references the RCL.

Additional resources
.NET MAUI Blazor podcast sample app
Source code (microsoft/dotnet-podcasts GitHub repository)
Live app
Share assets across web and native
clients using a Razor class library (RCL)
Article • 03/08/2023

Use a Razor class library (RCL) to share Razor components, C# code, and static assets
across web and native client projects.

This article builds on the general concepts found in the following articles:

Consume ASP.NET Core Razor components from a Razor class library (RCL)
Reusable Razor UI in class libraries with ASP.NET Core

The examples in this article share assets between a Blazor Server app and a .NET MAUI
Blazor Hybrid app in the same solution:

Although a Blazor Server app is used, the guidance applies equally to Blazor
WebAssembly apps sharing assets with a Blazor Hybrid app.
Projects are in the same solution, but an RCL can supply shared assets to projects
outside of a solution.
The RCL is added as a project to the solution, but any RCL can be published as a
NuGet package. A NuGet package can supply shared assets to web and native
client projects.
The order that the projects are created isn't important. However, projects that rely
on an RCL for assets must create a project reference to the RCL after the RCL is
created.

For guidance on creating an RCL, see Consume ASP.NET Core Razor components from a
Razor class library (RCL). Optionally, access the additional guidance on RCLs that apply
broadly to ASP.NET Core apps in Reusable Razor UI in class libraries with ASP.NET Core.

Sample app
For an example of the scenarios described in this article, see the .NET Podcasts sample
app:

GitHub repository (microsoft/dotnet-podcasts)


Running sample app (Azure Container Apps Service)

The .NET Podcasts app showcases the following technologies:

.NET
ASP.NET Core
Blazor
.NET MAUI
Azure Container Apps
Orleans

Share web UI Razor components, code, and


static assets
Components from an RCL can be simultaneously shared by web and native client apps
built using Blazor. The guidance in Consume ASP.NET Core Razor components from a
Razor class library (RCL) explains how to share Razor components using a Razor class
library (RCL). The same guidance applies to reusing Razor components from an RCL in a
Blazor Hybrid app.

Component namespaces are derived from the RCL's package ID or assembly name and
the component's folder path within the RCL. For more information, see ASP.NET Core
Razor components. @using directives can be placed in _Imports.razor files for
components and code, as the following example demonstrates for an RCL named
SharedLibrary with a Shared folder of shared Razor components and a Data folder of

shared data classes:

razor

@using SharedLibrary
@using SharedLibrary.Shared
@using SharedLibrary.Data

Place shared static assets in the RCL's wwwroot folder and update static asset paths in
the app to use the following path format:

_content/{PACKAGE ID/ASSEMBLY NAME}/{PATH}/{FILE NAME}

Placeholders:

{PACKAGE ID/ASSEMBLY NAME} : The package ID or assembly name of the RCL.

{PATH} : Optional path within the RCL's wwwroot folder.


{FILE NAME} : The file name of the static asset.

The preceding path format is also used in the app for static assets supplied by a NuGet
package added to the RCL.
For an RCL named SharedLibrary and using the minified Bootstrap stylesheet as an
example:

_content/SharedLibrary/css/bootstrap/bootstrap.min.css

For additional information on how to share static assets across projects, see the
following articles:

Consume ASP.NET Core Razor components from a Razor class library (RCL)
Reusable Razor UI in class libraries with ASP.NET Core

The root index.html file is usually specific to the app and should remain in the Blazor
Hybrid app or the Blazor WebAssembly app. The index.html file typically isn't shared.

The root Razor Component ( App.razor or Main.razor ) can be shared, but often might
need to be specific to the hosting app. For example, App.razor is different in the Blazor
Server and Blazor WebAssembly project templates when authentication is enabled. You
can add the AdditionalAssemblies parameter to specify the location of any shared
routable components, and you can specify a shared default layout component for the
router by type name.

Provide code and services independent of


hosting model
When code must differ across hosting models or target platforms, abstract the code as
interfaces and inject the service implementations in each project.

The following weather data example abstracts different weather forecast service
implementations:

Using an HTTP request for Blazor Hybrid and Blazor WebAssembly.


Requesting data directly for Blazor Server.

The example uses the following specifications and conventions:

The RCL is named SharedLibrary and contains the following folders and
namespaces:
Data : Contains the WeatherForecast class, which serves as a model for weather

data.
Interfaces : Contains the service interface for the service implementations,
named IWeatherForecastService .
The FetchData component is maintained in the Pages folder of the RCL, which is
routable by any of the apps consuming the RCL.
Each Blazor app maintains a service implementation that implements the
IWeatherForecastService interface.

Data/WeatherForecast.cs in the RCL:

C#

namespace SharedLibrary.Data;

public class WeatherForecast


{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

Interfaces/IWeatherForecastService.cs in the RCL:

C#

using SharedLibrary.Data;

namespace SharedLibrary.Interfaces;

public interface IWeatherForecastService


{
Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate);
}

The _Imports.razor file in the RCL includes the following added namespaces:

razor

@using SharedLibrary.Data
@using SharedLibrary.Interfaces

Services/WeatherForecastService.cs in the Blazor Hybrid and Blazor WebAssembly


apps:

C#

using System.Net.Http.Json;
using SharedLibrary.Data;
using SharedLibrary.Interfaces;
namespace {APP NAMESPACE}.Services;

public class WeatherForecastService : IWeatherForecastService


{
private readonly HttpClient http;

public WeatherForecastService(HttpClient http)


{
this.http = http;
}

public async Task<WeatherForecast[]?> GetForecastAsync(DateTime


startDate) =>
await http.GetFromJsonAsync<WeatherForecast[]?>("WeatherForecast");
}

In the preceding example, the {APP NAMESPACE} placeholder is the app's namespace.

Services/WeatherForecastService.cs in the Blazor Server app:

C#

using SharedLibrary.Data;
using SharedLibrary.Interfaces;

namespace {APP NAMESPACE}.Services;

public class WeatherForecastService : IWeatherForecastService


{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy",
"Hot"
};

public async Task<WeatherForecast[]?> GetForecastAsync(DateTime


startDate) =>
await Task.FromResult(Enumerable.Range(1, 5)
.Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
}).ToArray());
}

In the preceding example, the {APP NAMESPACE} placeholder is the app's namespace.

The Blazor Hybrid, Blazor WebAssembly, and Blazor Server apps register their weather
forecast service implementations ( Services.WeatherForecastService ) for
IWeatherForecastService .

The Blazor WebAssembly project also registers an HttpClient. The HttpClient registered
by default in an app created from the Blazor WebAssembly project template is sufficient
for this purpose. For more information, see Call a web API from an ASP.NET Core Blazor
app.

Pages/FetchData.razor in the RCL:

razor

@page "/fetchdata"
@inject IWeatherForecastService ForecastService

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

@if (forecasts == null)


{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}

@code {
private WeatherForecast[]? forecasts;

protected override async Task OnInitializedAsync()


{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}

Additional resources
Consume ASP.NET Core Razor components from a Razor class library (RCL)
Reusable Razor UI in class libraries with ASP.NET Core
CSS isolation support with Razor class libraries
Pass root component parameters in
ASP.NET Core Blazor Hybrid
Article • 04/20/2023

This article explains how to pass root component parameters in a Blazor Hybrid app.

The RootComponent class of a BlazorWebView defines a Parameters property of type


IDictionary<string, object?>? , which represents an optional dictionary of parameters

to pass to the root component:

.NET MAUI: Microsoft.AspNetCore.Components.WebView.Maui.RootComponent


WPF: Microsoft.AspNetCore.Components.WebView.Wpf.RootComponent
Windows Forms:
Microsoft.AspNetCore.Components.WebView.WindowsForms.RootComponent

The following example passes a view model to the root component, which further
passes the view model as a cascading type to a Razor component in the Blazor portion
of the app. The example is based on the keypad example in the .NET MAUI
documentation:

Data binding and MVVM: Commanding (.NET MAUI documentation): Explains data
binding with MVVM using a keypad example.
.NET MAUI Samples

Although the keypad example focuses on implementing the MVVM pattern in .NET
MAUI Blazor Hybrid apps:

The dictionary of objects passed to root components can include any type for any
purpose where you need to pass one or more parameters to the root component
for use by Razor components in the app.
The concepts demonstrated by the following .NET MAUI Blazor example are the
same for Windows Forms Blazor apps and WPF Blazor apps.

Place the following view model into your .NET MAUI Blazor Hybrid app.

KeypadViewModel.cs :

C#

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace MauiBlazor;

public class KeypadViewModel : INotifyPropertyChanged


{
public event PropertyChangedEventHandler PropertyChanged;

private string _inputString = "";


private string _displayText = "";
private char[] _specialChars = { '*', '#' };

public ICommand AddCharCommand { get; private set; }


public ICommand DeleteCharCommand { get; private set; }

public string InputString


{
get => _inputString;
private set
{
if (_inputString != value)
{
_inputString = value;
OnPropertyChanged();
DisplayText = FormatText(_inputString);

// Perhaps the delete button must be enabled/disabled.


((Command)DeleteCharCommand).ChangeCanExecute();
}
}
}

public string DisplayText


{
get => _displayText;
set
{
if (_displayText != value)
{
_displayText = value;
OnPropertyChanged();
}
}
}

public KeypadViewModel()
{
// Command to add the key to the input string
AddCharCommand = new Command<string>((key) => InputString += key);

// Command to delete a character from the input string when allowed


DeleteCharCommand =
new Command(
// Command will strip a character from the input string
() => InputString = InputString.Substring(0,
InputString.Length - 1),
// CanExecute is processed here to return true when there's
something to delete
() => InputString.Length > 0
);
}

string FormatText(string str)


{
bool hasNonNumbers = str.IndexOfAny(_specialChars) != -1;
string formatted = str;

// Format the string based on the type of data and the length
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
// Special characters exist, or the string is too small or large
for special formatting
// Do nothing
}

else if (str.Length < 8)


formatted = string.Format("{0}-{1}", str.Substring(0, 3),
str.Substring(3));

else
formatted = string.Format("({0}) {1}-{2}", str.Substring(0, 3),
str.Substring(3, 3), str.Substring(6));

return formatted;
}

public void OnPropertyChanged([CallerMemberName] string name = "") =>


PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

In this article's example, the app's root namespace is MauiBlazor . Change the
namespace of KeypadViewModel to match the app's root namespace:

C#

namespace MauiBlazor;

7 Note

At the time the KeypadViewModel view model was created for the .NET MAUI sample
app and the .NET MAUI documentation, view models were placed in a folder
named ViewModels , but the namespace was set to the root of the app and didn't
include the folder name. If you wish to update the namespace to include the folder
in the KeypadViewModel.cs file, modify the example code in this article to match.
Add using (C#) and @using (Razor) statements to the following files or fully-qualify
the references to the view model type as {APP
NAMESPACE}.ViewModels.KeypadViewModel , where the {APP NAMESPACE} placeholder is

the app's root namespace.

Although you can set Parameters directly in XAML, the following example names the
root component ( rootComponent ) in the XAML file and sets the parameter dictionary in
the code-behind file.

In MainPage.xaml :

XAML

<RootComponent x:Name="rootComponent"
Selector="#app"
ComponentType="{x:Type local:Main}" />

In the code-behind file ( MainPage.xaml.cs ), assign the view model in the constructor:

C#

public MainPage()
{
InitializeComponent();

rootComponent.Parameters =
new Dictionary<string, object>
{
{ "KeypadViewModel", new KeypadViewModel() }
};
}

The following example cascades the object ( KeypadViewModel ) down component


hierarchies in the Blazor portion of the app as a CascadingValue.

In the Main component ( Main.razor ):

Add a parameter matching the type of the object passed to the root component:

razor

@code {
[Parameter]
public KeypadViewModel KeypadViewModel { get; set; }
}
Cascade the KeypadViewModel with the CascadingValue component. Update the
<Found> XAML content to the following markup:

XAML

<Found Context="routeData">
<CascadingValue Value="@KeypadViewModel">
<RouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</CascadingValue>
</Found>

At this point, the cascaded type is available to Razor components throughout the app as
a CascadingParameter.

The following Keypad component example:

Displays the current value of KeypadViewModel.DisplayText .


Permits character deletion by calling the KeypadViewModel.DeleteCharCommand
command if the display string length is greater than 0 (zero), which is checked by
the call to ICommand.CanExecute.
Permits adding characters by calling KeypadViewModel.AddCharCommand with the key
pressed in the UI.

Pages/Keypad.razor :

razor

@page "/keypad"

<h1>Keypad</h1>

<table id="keypad">
<thead>
<tr>
<th colspan="2">@KeypadViewModel.DisplayText</th>
<th><button @onclick="DeleteChar">&#x21E6;</button></th>
</tr>
</thead>
<tbody>
<tr>
<td><button @onclick="@(e => AddChar("1"))">1</button></td>
<td><button @onclick="@(e => AddChar("2"))">2</button></td>
<td><button @onclick="@(e => AddChar("3"))">3</button></td>
</tr>
<tr>
<td><button @onclick="@(e => AddChar("4"))">4</button></td>
<td><button @onclick="@(e => AddChar("5"))">5</button></td>
<td><button @onclick="@(e => AddChar("6"))">6</button></td>
</tr>
<tr>
<td><button @onclick="@(e => AddChar("7"))">7</button></td>
<td><button @onclick="@(e => AddChar("8"))">8</button></td>
<td><button @onclick="@(e => AddChar("9"))">9</button></td>
</tr>
<tr>
<td><button @onclick="@(e => AddChar("*"))">*</button></td>
<td><button @onclick="@(e => AddChar("0"))">0</button></td>
<td><button @onclick="@(e => AddChar("#"))">#</button></td>
</tr>
</tbody>
</table>

@code {
[CascadingParameter]
protected KeypadViewModel KeypadViewModel { get; set; }

private void DeleteChar()


{
if (KeypadViewModel.DeleteCharCommand.CanExecute(null))
{
KeypadViewModel.DeleteCharCommand.Execute(null);
}
}

private void AddChar(string key)


{
KeypadViewModel.AddCharCommand.Execute(key);
}
}

Purely for demonstration purposes, style the buttons by placing the following CSS styles
in the wwwroot/index.html file's <head> content:

HTML

<style>
#keypad button {
border: 1px solid black;
border-radius:6px;
height: 35px;
width:80px;
}
</style>

Create a sidebar navigation entry in the NavMenu component ( Shared/NavMenu.razor )


with the following markup:

razor
<div class="nav-item px-3">
<NavLink class="nav-link" href="keypad">
<span class="oi oi-list-rich" aria-hidden="true"></span> Keypad
</NavLink>
</div>

Additional resources
Host a Blazor web app in a .NET MAUI app using BlazorWebView
Data binding and MVVM: Commanding (.NET MAUI documentation)
ASP.NET Core Blazor cascading values and parameters
ASP.NET Core Blazor Hybrid
authentication and authorization
Article • 03/24/2023

This article describes ASP.NET Core's support for the configuration and management of
security and ASP.NET Core Identity in Blazor Hybrid apps.

Authentication in Blazor Hybrid apps is handled by native platform libraries, as they


offer enhanced security guarantees that the browser sandbox can't offer. Authentication
of native apps uses an OS-specific mechanism or via a federated protocol, such as
OpenID Connect (OIDC) . Follow the guidance for the identity provider that you've
selected for the app and then further integrate identity with Blazor using the guidance
in this article.

Integrating authentication must achieve the following goals for Razor components and
services:

Use the abstractions in the Microsoft.AspNetCore.Components.Authorization


package, such as AuthorizeView.
React to changes in the authentication context.
Access credentials provisioned by the app from the identity provider, such as
access tokens to perform authorized API calls.

After authentication is added to a .NET MAUI, WPF, or Windows Forms app and users
are able to log in and log out successfully, integrate authentication with Blazor to make
the authenticated user available to Razor components and services. Perform the
following steps:

Reference the Microsoft.AspNetCore.Components.Authorization package.

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 .

Implement a custom AuthenticationStateProvider, which is the abstraction that


Razor components use to access information about the authenticated user and to
receive updates when the authentication state changes.
Register the custom authentication state provider in the dependency injection
container.

.NET MAUI apps use Xamarin.Essentials: Web Authenticator: The WebAuthenticator class
allows the app to initiate browser-based authentication flows that listen for a callback to
a specific URL registered with the app.

For additional guidance, see the following resources:

Web authenticator (.NET MAUI documentation


Sample.Server.WebAuthenticator sample app

Create a custom AuthenticationStateProvider


without user change updates
If the app authenticates the user immediately after the app launches and the
authenticated user remains the same for the entirety of the app lifetime, user change
notifications aren't required, and the app only provides information about the
authenticated user. In this scenario, the user logs into the app when the app is opened,
and the app displays the login screen again after the user logs out. The following
ExternalAuthStateProvider is an example implementation of a custom

AuthenticationStateProvider for this authentication scenario.

7 Note

The following custom AuthenticationStateProvider doesn't declare a namespace in


order to make the code example applicable to any Blazor Hybrid app. However, a
best practice is to provide your app's namespace when you implement the example
in a production app.

ExternalAuthStateProvider.cs :

C#

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider


{
private readonly Task<AuthenticationState> authenticationState;

public ExternalAuthStateProvider(AuthenticatedUser user) =>


authenticationState = Task.FromResult(new
AuthenticationState(user.Principal));

public override Task<AuthenticationState> GetAuthenticationStateAsync()


=>
authenticationState;
}

public class AuthenticatedUser


{
public ClaimsPrincipal Principal { get; set; } = new();
}

The following steps describe how to:

Add required namespaces.


Add the authorization services and Blazor abstractions to the service collection.
Build the service collection.
Resolve the AuthenticatedUser service to set the authenticated user's claims
principal. See your identity provider's documentation for details.
Return the built host.

In the MauiProgram.CreateMauiApp method of MauiProgram.cs , add namespaces for


Microsoft.AspNetCore.Components.Authorization and System.Security.Claims:

C#

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

Remove the following line of code that returns a built Microsoft.Maui.Hosting.MauiApp:

diff

- return builder.Build();

Replace the preceding line of code with the following code. Add OpenID/MSAL code to
authenticate the user. See your identity provider's documentation for details.

C#

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider,
ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();
var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>
();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity
provider's
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new


ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

return host;

Create a custom AuthenticationStateProvider


with user change updates
To update the user while the Blazor app is running, call
NotifyAuthenticationStateChanged within the AuthenticationStateProvider
implementation using either of the following approaches:

Signal an authentication update from outside of the BlazorWebView)


Handle authentication within the BlazorWebView

Signal an authentication update from outside of the


BlazorWebView (Option 1)

A custom AuthenticationStateProvider can use a global service to signal an


authentication update. We recommend that the service offer an event that the
AuthenticationStateProvider can subscribe to, where the event invokes
NotifyAuthenticationStateChanged.

7 Note

The following custom AuthenticationStateProvider doesn't declare a namespace in


order to make the code example applicable to any Blazor Hybrid app. However, a
best practice is to provide your app's namespace when you implement the example
in a production app.

ExternalAuthStateProvider.cs :
C#

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider


{
private AuthenticationState currentUser;

public ExternalAuthStateProvider(ExternalAuthService service)


{
currentUser = new AuthenticationState(service.CurrentUser);

service.UserChanged += (newUser) =>


{
currentUser = new AuthenticationState(newUser);
NotifyAuthenticationStateChanged(Task.FromResult(currentUser));
};
}

public override Task<AuthenticationState> GetAuthenticationStateAsync()


=>
Task.FromResult(currentUser);
}

public class ExternalAuthService


{
public event Action<ClaimsPrincipal>? UserChanged;
private ClaimsPrincipal? currentUser;

public ClaimsPrincipal CurrentUser


{
get { return currentUser ?? new(); }
set
{
currentUser = value;

if (UserChanged is not null)


{
UserChanged(currentUser);
}
}
}
}

In the MauiProgram.CreateMauiApp method of MauiProgram.cs , add a namespace for


Microsoft.AspNetCore.Components.Authorization:

C#
using Microsoft.AspNetCore.Components.Authorization;

Add the authorization services and Blazor abstractions to the service collection:

C#

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider,
ExternalAuthStateProvider>();
builder.Services.AddSingleton<ExternalAuthService>();

Wherever the app authenticates a user, resolve the ExternalAuthService service:

C#

var authService = host.Services.GetRequiredService<ExternalAuthService>();

Execute your custom OpenID/MSAL code to authenticate the user. See your identity
provider's documentation for details. The authenticated user ( authenticatedUser in the
following example) is a new ClaimsPrincipal based on a new ClaimsIdentity.

Set the current user to the authenticated user:

C#

authService.CurrentUser = authenticatedUser;

An alternative to the preceding approach is to set the user's principal on


System.Threading.Thread.CurrentPrincipal instead of setting it via a service, which avoids
use of the dependency injection container:

C#

public class CurrentThreadUserAuthenticationStateProvider :


AuthenticationStateProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
=>
Task.FromResult(
new AuthenticationState(Thread.CurrentPrincipal as
ClaimsPrincipal ??
new ClaimsPrincipal(new ClaimsIdentity())));
}
Using the alternative approach, only authorization services (AddAuthorizationCore) and
CurrentThreadUserAuthenticationStateProvider
( .TryAddScoped<AuthenticationStateProvider,
CurrentThreadUserAuthenticationStateProvider>() ) are added to the service collection.

Handle authentication within the BlazorWebView (Option


2)
A custom AuthenticationStateProvider can include additional methods to trigger log in
and log out and update the user.

7 Note

The following custom AuthenticationStateProvider doesn't declare a namespace in


order to make the code example applicable to any Blazor Hybrid app. However, a
best practice is to provide your app's namespace when you implement the example
in a production app.

ExternalAuthStateProvider.cs :

C#

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider


{
private ClaimsPrincipal currentUser = new ClaimsPrincipal(new
ClaimsIdentity());

public override Task<AuthenticationState> GetAuthenticationStateAsync()


=>
Task.FromResult(new AuthenticationState(currentUser));

public Task LogInAsync()


{
var loginTask = LogInAsyncCore();
NotifyAuthenticationStateChanged(loginTask);

return loginTask;

async Task<AuthenticationState> LogInAsyncCore()


{
var user = await LoginWithExternalProviderAsync();
currentUser = user;
return new AuthenticationState(currentUser);
}
}

private Task<ClaimsPrincipal> LoginWithExternalProviderAsync()


{
/*
Provide OpenID/MSAL code to authenticate the user. See your
identity
provider's documentation for details.

Return a new ClaimsPrincipal based on a new ClaimsIdentity.


*/
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity());

return Task.FromResult(authenticatedUser);
}

public void Logout()


{
currentUser = new ClaimsPrincipal(new ClaimsIdentity());
NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(currentUser)));
}
}

In the preceding example:

The call to LogInAsyncCore triggers the login process.


The call to NotifyAuthenticationStateChanged notifies that an update is in
progress, which allows the app to provide a temporary UI during the login or
logout process.
Returning loginTask returns the task so that the component that triggered the
login can await and react after the task is complete.
The LoginWithExternalProviderAsync method is implemented by the developer to
log in the user with the identity provider's SDK. For more information, see your
identity provider's documentation. The authenticated user ( authenticatedUser ) is a
new ClaimsPrincipal based on a new ClaimsIdentity.

In the MauiProgram.CreateMauiApp method of MauiProgram.cs , add the authorization


services and the Blazor abstraction to the service collection:

C#

builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider,
ExternalAuthStateProvider>();
The following LoginComponent component demonstrates how to log in a user. In a
typical app, the LoginComponent component is only shown in a parent component if the
user isn't logged into the app.

Shared/LoginComponent.razor :

razor

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Login">Log in</button>

@code
{
public async Task Login()
{
await ((ExternalAuthStateProvider)AuthenticationStateProvider)
.LogInAsync();
}
}

The following LogoutComponent component demonstrates how to log out a user. In a


typical app, the LogoutComponent component is only shown in a parent component if the
user is logged into the app.

Shared/LogoutComponent.razor :

razor

@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Logout">Log out</button>

@code
{
public async Task Logout()
{
await ((ExternalAuthStateProvider)AuthenticationStateProvider)
.Logout();
}
}

Accessing other authentication information


Blazor doesn't define an abstraction to deal with other credentials, such as access tokens
to use for HTTP requests to web APIs. We recommend following the identity provider's
guidance to manage the user's credentials with the primitives that the identity provider's
SDK provides.

It's common for identity provider SDKs to use a token store for user credentials stored in
the device. If the SDK's token store primitive is added to the service container, consume
the SDK's primitive within the app.

The Blazor framework isn't aware of a user's authentication credentials and doesn't
interact with credentials in any way, so the app's code is free to follow whatever
approach you deem most convenient. However, follow the general security guidance in
the next section, Other authentication security considerations, when implementing
authentication code in an app.

Other authentication security considerations


The authentication process is external to Blazor, and we recommend that developers
access the identity provider's guidance for additional security guidance.

When implementing authentication:

Avoid authentication in the context of the Web View. For example, avoid using a
JavaScript OAuth library to perform the authentication flow. In a single-page app,
authentication tokens aren't hidden in JavaScript and can be easily discovered by
malicious users and used for nefarious purposes. Native apps don't suffer this risk
because native apps are only able to obtain tokens outside of the browser context,
which means that rogue third-party scripts can't steal the tokens and compromise
the app.
Avoid implementing the authentication workflow yourself. In most cases, platform
libraries securely handle the authentication workflow, using the system's browser
instead of using a custom Web View that can be hijacked.
Avoid using the platform's Web View control to perform authentication. Instead,
rely on the system's browser when possible.
Avoid passing the tokens to the document context (JavaScript). In some situations,
a JavaScript library within the document is required to perform an authorized call
to an external service. Instead of making the token available to JavaScript via JS
interop:
Provide a generated temporary token to the library and within the Web View.
Intercept the outgoing network request in code.
Replace the temporary token with the real token and confirm that the
destination of the request is valid.
Additional resources
ASP.NET Core Blazor authentication and authorization
ASP.NET Core Blazor Hybrid security considerations
ASP.NET Core Blazor Hybrid security
considerations
Article • 03/13/2023

This article describes security considerations for Blazor Hybrid apps.

Blazor Hybrid apps that render web content execute .NET code inside a platform Web
View. The .NET code interacts with the web content via an interop channel between the
.NET code and the Web View.

The web content rendered into the Web View can come from assets provided by the
app from either of the following locations:

The wwwroot folder in the app.


A source external to the app. For example, a network source, such as the Internet.

A trust boundary exists between the .NET code and the code that runs inside the Web
View. .NET code is provided by the app and any trusted third-party packages that you've
installed. After the app is built, the .NET code Web View content sources can't change.

In contrast to the .NET code sources of content, content sources from the code that runs
inside the Web View can come not only from the app but also from external sources. For
example, static assets from an external Content Delivery Network (CDN) might be used
or rendered by an app's Web View.

Consider the code inside the Web View as untrusted in the same way that code running
inside the browser for a web app isn't trusted. The same threats and general security
recommendations apply to untrusted resources in Blazor Hybrid apps as for other types
of apps.

If possible, avoid loading content from a third-party origin. To mitigate risk, you might
be able to serve content directly from the app by downloading the external assets,
verifying that they're safe to serve to users, and placing them into the app's wwwroot
folder for packaging with the rest of the app. When the external content is downloaded
for inclusion in the app, we recommend scanning it for viruses and malware before
placing it into the wwwroot folder of the app.

If your app must reference content from an external origin, we recommend that you use
common web security approaches to provide the app with an opportunity to block the
content from loading if the content is compromised:

Serve content securely with TLS/HTTPS.


Institute a Content Security Policy (CSP) .
Perform subresource integrity checks.

Even if all of the resources are packed into the app and don't load from any external
origin, remain cautious about problems in the resources' code that run inside the Web
View, as the resources might have vulnerabilities that could allow cross-site scripting
(XSS) attacks.

In general, the Blazor framework protects against XSS by dealing with HTML in safe
ways. However, some programming patterns allow Razor components to inject raw
HTML into rendered output, such as rendering content from an untrusted source. For
example, rendering HTML content directly from a database should be avoided.
Additionally, JavaScript libraries used by the app might manipulate HTML in unsafe ways
to inadvertently or deliberately render unsafe output.

For these reasons, it's best to apply the same protections against XSS that are normally
applied to web apps. Prevent loading scripts from unknown sources and don't
implement potentially unsafe JavaScript features, such as eval and other unsafe
JavaScript primitives. Establishing a CSP is recommended to reduce these security risks.

If the code inside the Web View is compromised, the code gains access to all of the
content inside the Web View and might interact with the host via the interop channel.
For that reason, any content coming from the Web View (events, JS interop) must be
treated as untrusted and validated in the same way as for other sensitive contexts, such
as in a compromised Blazor Server app that can lead to malicious attacks on the host
system.

Don't store sensitive information, such as credentials, security tokens, or sensitive user
data, in the context of the Web View, as it makes the information available to an attacker
if the Web View is compromised. There are safer alternatives, such as handling the
sensitive information directly within the native portion of the app.
External content rendered in an iframe
When using an iframe to display external content within a Blazor Hybrid page, we
recommend that users leverage sandboxing features to ensure that the content is
isolated from the parent page containing the app. In the following Razor component
example, the sandbox attribute is present for the <iframe> tag to apply sandboxing
features to the foo.html page:

razor

<iframe sandbox src="https://contoso.com/foo.html" />

2 Warning

The sandbox attribute is not supported in early browser versions. For more
information, see Can I use: sandbox .

Links to external URLs


By default, links to URLs outside of the app are opened in an appropriate external app,
not loaded within the Web View. We do not recommend overriding the default behavior.

Keep the Web View current in deployed apps


By default, the BlazorWebView control uses the currently-installed, platform-specific
native Web View. Since the native Web View is periodically updated with support for
new APIs and fixes for security issues, it may be necessary to ensure that an app is using
a Web View version that meets the app's requirements.

Use one of the following approaches to keep the Web View current in deployed apps:

On all platforms: Check the Web View version and prompt the user to take any
necessary steps to update it.
Only on Windows: Package a fixed-version Web View within the app, using it in
place of the system's shared Web View.

Android
The Android Web View is distributed and updated via the Google Play Store . Check
the Web View version by reading the User-Agent string. Read the Web View's
navigator.userAgent property using JavaScript interop and optionally cache the value
using a singleton service if the user agent string is required outside of a Razor
component context.

iOS/Mac Catalyst
iOS and Mac Catalyst both use WKWebView , a Safari-based control, which is updated
by the operating system. Similar to the Android case, determine the Web View version
by reading the Web View's User-Agent string.

Windows (.NET MAUI, WPF, Windows Forms)


On Windows, the Chromium-based Microsoft Edge WebView2 is required to run Blazor
web apps.

By default, the newest installed version of WebView2 , known as the Evergreen distribution,
is used. If you wish to ship a specific version of WebView2 with the app, use the Fixed
Version distribution.

For more information on checking the currently-installed WebView2 version and the
distribution modes, see the WebView2 distribution documentation.

Additional resources
ASP.NET Core Blazor Hybrid authentication and authorization
ASP.NET Core Blazor authentication and authorization
Publish ASP.NET Core Blazor Hybrid
apps
Article • 04/04/2023

This article explains how to publish Blazor Hybrid apps.

Publish for a specific framework


Blazor Hybrid supports .NET MAUI, WPF, and Windows Forms. The publishing steps for
apps using Blazor Hybrid are nearly identical to the publishing steps for the target
platform.

WPF and Windows Forms


.NET application publishing overview
.NET MAUI
Windows
Android
iOS
macOS

Blazor-specific considerations
Blazor Hybrid apps require a Web View on the host platform. For more information, see
Keep the Web View current in deployed Blazor Hybrid apps.
ASP.NET Core Blazor project structure
Article • 06/22/2023

This article describes the files and folders that make up a Blazor app generated from a
Blazor project template.

Blazor Server
Blazor Server project templates: blazorserver , blazorserver-empty

The Blazor Server templates create the initial files and directory structure for a Blazor
Server app:

If the blazorserver template is used, the app is populated with the following:
Demonstration code for a FetchData component that loads data from a weather
forecast service ( WeatherForecastService ) and user interaction with a Counter
component.
Bootstrap frontend toolkit.
If the blazorserver-empty template is used, the app is created without
demonstration code and Bootstrap.

Project structure:

Data folder: Contains the WeatherForecast class and implementation of the

WeatherForecastService that provides example weather data to the app's


FetchData component.

Pages folder: Contains the Blazor app's routable Razor components ( .razor ) and
the root Razor page of a Blazor Server app. The route for each page is specified
using the @page directive. The template includes the following:
_Host.cshtml : The root page of the app implemented as a Razor Page:

When any page of the app is initially requested, this page is rendered and
returned in the response.
The Host page specifies where the root App component ( App.razor ) is
rendered.
Counter component ( Counter.razor ): Implements the Counter page.

Error component ( Error.razor ): Rendered when an unhandled exception

occurs in the app.


FetchData component ( FetchData.razor ): Implements the Fetch data page.

Index component ( Index.razor ): Implements the Home page.


Properties folder: Holds development environment configuration in the

launchSettings.json file.

Shared folder: Contains the following shared components and stylesheets:

MainLayout component ( MainLayout.razor ): The app's layout component.


MainLayout.razor.css : Stylesheet for the app's main layout.

NavMenu component ( NavMenu.razor ): Implements sidebar navigation. Includes

the NavLink component (NavLink), which renders navigation links to other Razor
components. The NavLink component automatically indicates a selected state
when its component is loaded, which helps the user understand which
component is currently displayed.
NavMenu.razor.css : Stylesheet for the app's navigation menu.

SurveyPrompt component ( SurveyPrompt.razor ): Blazor survey component.

wwwroot folder: The Web Root folder for the app containing the app's public static

assets.

_Imports.razor : Includes common Razor directives to include in the app's

components ( .razor ), such as @using directives for namespaces.

App.razor : The root component of the app that sets up client-side routing using
the Router component. The Router component intercepts browser navigation and
renders the page that matches the requested address.

appsettings.json and environmental app settings files: Provide configuration

settings for the app.

Program.cs : The app's entry point that sets up the ASP.NET Core host and contains

the app's startup logic, including service registrations and request processing
pipeline configuration:
Specifies the app's dependency injection (DI) services. Services are added by
calling AddServerSideBlazor, and the WeatherForecastService is added to the
service container for use by the example FetchData component.
Configures the app's request handling pipeline:
MapBlazorHub is called to set up an endpoint for the real-time connection
with the browser. The connection is created with SignalR, which is a
framework for adding real-time web functionality to apps.
MapFallbackToPage("/_Host") is called to set up the root page of the app
( Pages/_Host.cshtml ) and enable navigation.

Additional files and folders may appear in an app produced from a Blazor Server project
template when additional options are configured. For example, generating an app with
ASP.NET Core Identity includes additional assets for authentication and authorization
features.

Blazor WebAssembly
Blazor WebAssembly project templates: blazorwasm , blazorwasm-empty

The Blazor WebAssembly templates create the initial files and directory structure for a
Blazor WebAssembly app:

If the blazorwasm template is used, the app is populated with the following:
Demonstration code for a FetchData component that loads data from a static
asset ( weather.json ) and user interaction with a Counter component.
Bootstrap frontend toolkit.
If the blazorwasm-empty template is used, the app is created without
demonstration code and Bootstrap.

Project structure:

Pages folder: Contains the Blazor app's routable Razor components ( .razor ). The
route for each page is specified using the @page directive. The template includes
the following components:
Counter component ( Counter.razor ): Implements the Counter page.

FetchData component ( FetchData.razor ): Implements the Fetch data page.

Index component ( Index.razor ): Implements the Home page.

Properties folder: Holds development environment configuration in the

launchSettings.json file.

Shared folder: Contains the following shared components and stylesheets:

MainLayout component ( MainLayout.razor ): The app's layout component.

MainLayout.razor.css : Stylesheet for the app's main layout.


NavMenu component ( NavMenu.razor ): Implements sidebar navigation. Includes

the NavLink component (NavLink), which renders navigation links to other Razor
components. The NavLink component automatically indicates a selected state
when its component is loaded, which helps the user understand which
component is currently displayed.
NavMenu.razor.css : Stylesheet for the app's navigation menu.

SurveyPrompt component ( SurveyPrompt.razor ) (ASP.NET Core 7.0 or earlier):


Blazor survey component.
wwwroot folder: The Web Root folder for the app containing the app's public static

assets, including appsettings.json and environmental app settings files for


configuration settings. The index.html webpage is the root page of the app
implemented as an HTML page:
When any page of the app is initially requested, this page is rendered and
returned in the response.
The page specifies where the root App component is rendered. The component
is rendered at the location of the div DOM element with an id of app ( <div
id="app">Loading...</div> ).

_Imports.razor : Includes common Razor directives to include in the app's

components ( .razor ), such as @using directives for namespaces.

App.razor : The root component of the app that sets up client-side routing using
the Router component. The Router component intercepts browser navigation and
renders the page that matches the requested address.

Program.cs : The app's entry point that sets up the WebAssembly host:

The App component is the root component of the app. The App component is
specified as the div DOM element with an id of app ( <div
id="app">Loading...</div> in wwwroot/index.html ) to the root component

collection ( builder.RootComponents.Add<App>("#app") ).
Services are added and configured (for example,
builder.Services.AddSingleton<IMyDependency, MyDependency>() ).

Additional files and folders may appear in an app produced from a Blazor WebAssembly
project template when additional options are configured. For example, generating an
app with ASP.NET Core Identity includes additional assets for authentication and
authorization features.

A hosted Blazor WebAssembly solution includes the following ASP.NET Core projects:

"Client": The Blazor WebAssembly app.


"Server": An app that serves the Blazor WebAssembly app and weather data to
clients.
"Shared": A project that maintains common classes, methods, and resources.

The solution is generated from the Blazor WebAssembly project template in Visual
Studio with the ASP.NET Core Hosted checkbox selected or with the -ho|--hosted
option using the .NET CLI's dotnet new blazorwasm command. For more information, see
Tooling for ASP.NET Core Blazor.
The project structure of the client-side app in a hosted Blazor Webassembly solution
("Client" project) is the same as the project structure for a standalone Blazor
WebAssembly app. Additional files in a hosted Blazor WebAssembly solution:

The "Server" project includes a weather forecast controller at


Controllers/WeatherForecastController.cs that returns weather data to the

"Client" project's FetchData component.


The "Shared" project includes a weather forecast class at WeatherForecast.cs that
represents weather data for the "Client" and "Server" projects.

Location of <head> content


In a Blazor Server app, <head> content is located in the Pages/_Host.cshtml file.

In a Blazor WebAssembly app, <head> content is located in the wwwroot/index.html file.

Dual Blazor Server/Blazor WebAssembly app


To create an app that can run as either a Blazor Server app or a Blazor WebAssembly
app, one approach is to place all of the app logic and components into a Razor class
library (RCL) and reference the RCL from separate Blazor Server and Blazor
WebAssembly projects. For common services whose implementations differ based on
the hosting model, define the service interfaces in the RCL and implement the services
in the Blazor Server and Blazor WebAssembly projects.

Additional resources
Tooling for ASP.NET Core Blazor
ASP.NET Core Blazor hosting models
Minimal APIs quick reference
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor fundamentals
Article • 04/04/2023

Fundamentals articles provide guidance on foundational Blazor concepts. Some of the


concepts are connected to a basic understanding of Razor components, which are
described further in the next section of this article and covered in detail in the
Components articles.

Razor components
Blazor apps are based on Razor components, often referred to as just components. A
component is an element of UI, such as a page, dialog, or data entry form. Components
are .NET C# classes built into .NET assemblies.

Razor refers to how components are usually written in the form of a Razor markup page
for client-side UI logic and composition. Razor is a syntax for combining HTML markup
with C# code designed for developer productivity. Razor files use the .razor file
extension.

Although some Blazor developers and online resources use the term "Blazor
components," the documentation avoids that term and universally uses "Razor
components" or "components."

Blazor documentation adopts several conventions for showing and discussing


components:

Project code, file paths and names, project template names, and other specialized
terms are in United States English and usually code-fenced.
Components are usually referred to by their C# class name (Pascal case) followed
by the word "component." For example, a typical file upload component is referred
to as the " FileUpload component."
Usually, a component's C# class name is the same as its file name. Component
paths within an app are usually indicated. For example, Pages/FileUpload.razor .
Routable components usually set their relative URLs to the component's class
name in kebab-case. For example, a FileUpload component includes routing
configuration to reach the rendered component at the relative URL /file-upload .
Routing and navigation is covered in ASP.NET Core Blazor routing and navigation.
When multiple versions of a component are used, they're numbered sequentially.
For example, the FileUpload3 component has a file name and location of
Pages/FileUpload3.razor and is reached at /file-upload-3 .
Access modifiers are used in article examples. For example, fields are private by
default but are explicitly present in component code. For example, private is
stated for declaring a field named maxAllowedFiles as private int
maxAllowedFiles = 3; .
Generally, examples adhere to ASP.NET Core/C# coding conventions and
engineering guidelines. For more information see the following resources:
Engineering guidelines (dotnet/aspnetcore GitHub repository)
C# Coding Conventions (C# guide)

The following is an example counter component and part of an app created from a
Blazor project template. Detailed components coverage is found in the Components
articles later in the documentation. The following example demonstrates component
concepts seen in the Fundamentals articles before reaching the Components articles
later in the documentation.

Pages/Counter.razor :

razor

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

The preceding Counter component:

Sets its route with the @page directive in the first line.
Sets its page title and heading.
Renders the current count with @currentCount . currentCount is an integer variable
defined in the C# code of the @code block.
Displays a button to trigger the IncrementCount method, which is also found in the
@code block and increases the value of the currentCount variable.
Sample apps
Documentation sample apps are available for inspection and download:

Blazor samples GitHub repository (dotnet/blazor-samples)

The repo contains two types of samples:

Snippet sample apps for Blazor Server and Blazor WebAssembly provide the code
examples that appear in Blazor articles. These apps don't compile and aren't
runnable apps. They're provided solely for the purpose of obtaining article
example code.
Samples apps to accompany Blazor articles compile and run for the following
scenarios:
Blazor Server with EF Core
Blazor Server and Blazor WebAssembly with SignalR
Blazor WebAssembly scopes-enabled logging

7 Note

Not all of the preceding sample apps are available for all releases of ASP.NET Core.

For more information, see the Blazor samples GitHub repository README.md file .

The ASP.NET Core repository's Basic Test App is also a helpful set of samples for various
Blazor scenarios:

BasicTestApp in ASP.NET Core reference source (dotnet/aspnetcore)

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Byte multiples
.NET byte sizes use metric prefixes for non-decimal multiples of bytes based on powers
of 1024.
Name (abbreviation) Size Example

Kilobyte (KB) 1,024 bytes 1 KB = 1,024 bytes

Megabyte (MB) 1,0242 bytes 1 MB = 1,048,576 bytes

Gigabyte (GB) 1,0243 bytes 1 GB = 1,073,741,824 bytes

Support requests
Only documentation-related issues are appropriate for the dotnet/AspNetCore.Docs
repository. For product support, don't open a documentation issue. Seek assistance
through one or more of the following support channels:

Stack Overflow (tagged: blazor)


General ASP.NET Core Slack Team
Blazor Gitter

For a potential bug in the framework or product feedback, open an issue for the
ASP.NET Core product unit at dotnet/aspnetcore issues . Bug reports usually require
the following:

Clear explanation of the problem: Follow the instructions in the GitHub issue
template provided by the product unit when opening the issue.
Minimal repro project: Place a project on GitHub for the product unit engineers to
download and run. Cross-link the project into the issue's opening comment.

For a potential problem with a Blazor article, open a documentation issue. To open a
documentation issue, use the This page feedback button and form at the bottom of the
article and leave the metadata in place when creating the opening comment. The
metadata provides tracking data and automatically pings the author of the article. If the
subject was discussed with the product unit, place a cross-link to the engineering issue
in the documentation issue's opening comment.

For problems or feedback on Visual Studio or Visual Studio for Mac, use the Report a
Problem or Suggest a Feature gestures from within Visual Studio, which open internal
issues for Visual Studio teams. For more information, see Visual Studio Feedback or
How to report a problem in Visual Studio for Mac.

For problems with Visual Studio Code, ask for support on community support forums.
For bug reports and product feedback, open an issue on the microsoft/vscode GitHub
repo .
GitHub issues for Blazor documentation are automatically marked for triage on the
Blazor.Docs project (dotnet/AspNetCore.Docs GitHub repository) . Please wait a short
while for a response, especially over weekends and holidays. Usually, documentation
authors respond within 24 hours on weekdays.

Community links to Blazor resources


For a collection of links to Blazor resources maintained by the community, visit
Awesome Blazor .

7 Note

Microsoft doesn't own, maintain, or support Awesome Blazor and most of the
community products and services described and linked there.
ASP.NET Core Blazor routing and navigation
Article • 03/06/2023

This article explains how to manage request routing and how to use the NavLink component to create navigation
links in Blazor apps.

) Important

Code examples throughout this article show methods called on Navigation , which is an injected
NavigationManager in classes and components.

Route templates
The Router component enables routing to Razor components in a Blazor app. The Router component is used in
the App component of Blazor apps.

App.razor :

razor

<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>

When a Razor component ( .razor ) with an @page directive is compiled, the generated component class is
provided a RouteAttribute specifying the component's route template.

When the app starts, the assembly specified as the Router's AppAssembly is scanned to gather route information
for the app's components that have a RouteAttribute.

At runtime, the RouteView component:

Receives the RouteData from the Router along with any route parameters.
Renders the specified component with its layout, including any further nested layouts.

Optionally specify a DefaultLayout parameter with a layout class for components that don't specify a layout with
the @layout directive. The framework's Blazor project templates specify the MainLayout component
( Shared/MainLayout.razor ) as the app's default layout. For more information on layouts, see ASP.NET Core Blazor
layouts.

Components support multiple route templates using multiple @page directives. The following example
component loads on requests for /blazor-route and /different-blazor-route .

Pages/BlazorRoute.razor :

razor

@page "/blazor-route"
@page "/different-blazor-route"
<h1>Blazor routing</h1>

) Important

For URLs to resolve correctly, the app must include a <base> tag (location of <head> content) with the app
base path specified in the href attribute. For more information, see Host and deploy ASP.NET Core Blazor.

As an alternative to specifying the route template as a string literal with the @page directive, constant-based route
templates can be specified with the @attribute directive.

In the following example, the @page directive in a component is replaced with the @attribute directive and the
constant-based route template in Constants.CounterRoute , which is set elsewhere in the app to " /counter ":

diff

- @page "/counter"
+ @attribute [Route(Constants.CounterRoute)]

Focus an element on navigation


Use the FocusOnNavigate component to set the UI focus to an element based on a CSS selector after navigating
from one page to another. You can see the FocusOnNavigate component in use by the App component of an app
generated from a Blazor project template.

In App.razor :

razor

<FocusOnNavigate RouteData="@routeData" Selector="h1" />

When the Router component navigates to a new page, the FocusOnNavigate component sets the focus to the
page's top-level header ( <h1> ). This is a common strategy for ensuring that a page navigation is announced when
using a screen reader.

Provide custom content when content isn't found


The Router component allows the app to specify custom content if content isn't found for the requested route.

In the App component, set custom content in the Router component's NotFound template.

App.razor :

razor

<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<h1>Sorry</h1>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>
Arbitrary items are supported as content of the <NotFound> tags, such as other interactive components. To apply a
default layout to NotFound content, see ASP.NET Core Blazor layouts.

Route to components from multiple assemblies


Use the AdditionalAssemblies parameter to specify additional assemblies for the Router component to consider
when searching for routable components. Additional assemblies are scanned in addition to the assembly specified
to AppAssembly . In the following example, Component1 is a routable component defined in a referenced component
class library. The following AdditionalAssemblies example results in routing support for Component1 .

App.razor :

razor

<Router
AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="new[] { typeof(Component1).Assembly }">
@* ... Router component elements ... *@
</Router>

Route parameters
The router uses route parameters to populate the corresponding component parameters with the same name.
Route parameter names are case insensitive. In the following example, the text parameter assigns the value of the
route segment to the component's Text property. When a request is made for /route-parameter-1/amazing , the
<h1> tag content is rendered as Blazor is amazing! .

Pages/RouteParameter1.razor :

razor

@page "/route-parameter-1/{text}"

<h1>Blazor is @Text!</h1>

@code {
[Parameter]
public string? Text { get; set; }
}

Optional parameters are supported. In the following example, the text optional parameter assigns the value of
the route segment to the component's Text property. If the segment isn't present, the value of Text is set to
fantastic .

Pages/RouteParameter2.razor :

razor

@page "/route-parameter-2/{text?}"

<h1>Blazor is @Text!</h1>

@code {
[Parameter]
public string? Text { get; set; }

protected override void OnInitialized()


{
Text = Text ?? "fantastic";
}
}

Use OnParametersSet instead of OnInitialized{Async} to permit app navigation to the same component with a
different optional parameter value. Based on the preceding example, use OnParametersSet when the user should
be able to navigate from /route-parameter-2 to /route-parameter-2/amazing or from /route-parameter-2/amazing
to /route-parameter-2 :

C#

protected override void OnParametersSet()


{
Text = Text ?? "fantastic";
}

Route constraints
A route constraint enforces type matching on a route segment to a component.

In the following example, the route to the User component only matches if:

An Id route segment is present in the request URL.


The Id segment is an integer ( int ) type.

Pages/User.razor :

razor

@page "/user/{Id:int}"

<h1>User Id: @Id</h1>

@code {
[Parameter]
public int Id { get; set; }
}

The route constraints shown in the following table are available. For the route constraints that match the invariant
culture, see the warning below the table for more information.

Constraint Example Example Matches Invariant


culture
matching

bool {active:bool} true , FALSE No

datetime {dob:datetime} 2016-12-31 , 2016-12-31 7:32pm Yes

decimal {price:decimal} 49.99 , -1,000.01 Yes

double {weight:double} 1.234 , -1,001.01e8 Yes

float {weight:float} 1.234 , -1,001.01e8 Yes

guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 , {CD2C1638-1638-72D5-1638-DEADBEEF1638} No

int {id:int} 123456789 , -123456789 Yes


Constraint Example Example Matches Invariant
culture
matching

long {ticks:long} 123456789 , -123456789 Yes

2 Warning

Route constraints that verify the URL and are converted to a CLR type (such as int or DateTime) always use
the invariant culture. These constraints assume that the URL is non-localizable.

Route constraints also work with optional parameters. In the following example, Id is required, but Option is an
optional boolean route parameter.

Pages/User.razor :

razor

@page "/user/{Id:int}/{Option:bool?}"

<p>
Id: @Id
</p>

<p>
Option: @Option
</p>

@code {
[Parameter]
public int Id { get; set; }

[Parameter]
public bool Option { get; set; }
}

Routing with URLs that contain dots


For hosted Blazor WebAssembly and Blazor Server apps, the server-side default route template assumes that if the
last segment of a request URL contains a dot ( . ) that a file is requested. For example, the relative URL
/example/some.thing is interpreted by the router as a request for a file named some.thing . Without additional
configuration, an app returns a 404 - Not Found response if some.thing was meant to route to a component with
an @page directive and some.thing is a route parameter value. To use a route with one or more parameters that
contain a dot, the app must configure the route with a custom template.

Consider the following Example component that can receive a route parameter from the last segment of the URL.

Pages/Example.razor :

razor

@page "/example/{param?}"

<p>
Param: @Param
</p>

@code {
[Parameter]
public string? Param { get; set; }
}

To permit the Server app of a hosted Blazor WebAssembly solution to route the request with a dot in the param
route parameter, add a fallback file route template with the optional parameter in Program.cs :

C#

app.MapFallbackToFile("/example/{param?}", "index.html");

To configure a Blazor Server app to route the request with a dot in the param route parameter, add a fallback page
route template with the optional parameter in Program.cs :

C#

app.MapFallbackToPage("/example/{param?}", "/_Host");

For more information, see Routing in ASP.NET Core.

Catch-all route parameters


Catch-all route parameters, which capture paths across multiple folder boundaries, are supported in components.

Catch-all route parameters are:

Named to match the route segment name. Naming isn't case-sensitive.


A string type. The framework doesn't provide automatic casting.
At the end of the URL.

Pages/CatchAll.razor :

razor

@page "/catch-all/{*pageRoute}"

@code {
[Parameter]
public string? PageRoute { get; set; }
}

For the URL /catch-all/this/is/a/test with a route template of /catch-all/{*pageRoute} , the value of PageRoute
is set to this/is/a/test .

Slashes and segments of the captured path are decoded. For a route template of /catch-all/{*pageRoute} , the
URL /catch-all/this/is/a%2Ftest%2A yields this/is/a/test* .

URI and navigation state helpers


Use NavigationManager to manage URIs and navigation in C# code. NavigationManager provides the event and
methods shown in the following table.

Member Description
Member Description

Uri Gets the current absolute URI.

BaseUri Gets the base URI (with a trailing slash) that can be prepended to relative URI paths to
produce an absolute URI. Typically, BaseUri corresponds to the href attribute on the
document's <base> element (location of <head> content).

NavigateTo Navigates to the specified URI. If forceLoad is true :

Client-side routing is bypassed.


The browser is forced to load the new page from the server, whether or not the URI
is normally handled by the client-side router.

If replace is true , the current URI in the browser history is replaced instead of pushing a
new URI onto the history stack.

LocationChanged An event that fires when the navigation location has changed. For more information, see
the Location changes section.

ToAbsoluteUri Converts a relative URI into an absolute URI.

ToBaseRelativePath Given a base URI (for example, a URI previously returned by BaseUri), converts an absolute
URI into a URI relative to the base URI prefix.

RegisterLocationChangingHandler Registers a handler to process incoming navigation events. Calling NavigateTo always
invokes the handler.

GetUriWithQueryParameter Returns a URI constructed by updating NavigationManager.Uri with a single parameter


added, updated, or removed. For more information, see the Query strings section.

Location changes
For the LocationChanged event, LocationChangedEventArgs provides the following information about navigation
events:

Location: The URL of the new location.


IsNavigationIntercepted: If true , Blazor intercepted the navigation from the browser. If false ,
NavigationManager.NavigateTo caused the navigation to occur.

The following component:

Navigates to the app's Counter component ( Pages/Counter.razor ) when the button is selected using
NavigateTo.
Handles the location changed event by subscribing to NavigationManager.LocationChanged.

The HandleLocationChanged method is unhooked when Dispose is called by the framework. Unhooking the
method permits garbage collection of the component.

The logger implementation logs the following information when the button is selected:

BlazorSample.Pages.Navigate: Information: URL of new location: https://localhost:{PORT}/counter

Pages/Navigate.razor :

razor

@page "/navigate"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<Navigate> Logger
@inject NavigationManager Navigation

<h1>Navigate in component code example</h1>

<button class="btn btn-primary" @onclick="NavigateToCounterComponent">


Navigate to the Counter component
</button>

@code {
private void NavigateToCounterComponent()
{
Navigation.NavigateTo("counter");
}

protected override void OnInitialized()


{
Navigation.LocationChanged += HandleLocationChanged;
}

private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)


{
Logger.LogInformation("URL of new location: {Location}", e.Location);
}

public void Dispose()


{
Navigation.LocationChanged -= HandleLocationChanged;
}
}

For more information on component disposal, see ASP.NET Core Razor component lifecycle.

Navigation history state


The NavigationManager uses the browser's History API to maintain navigation history state associated with each
location change made by the app. Maintaining history state is particularly useful in external redirect scenarios, such
as when authenticating users with external identity providers. For more information, see the Navigation options
section.

Navigation options
Pass NavigationOptions to NavigateTo to control the following behaviors:

ForceLoad: Bypass client-side routing and force the browser to load the new page from the server, whether
or not the URI is handled by the client-side router. The default value is false .
ReplaceHistoryEntry: Replace the current entry in the history stack. If false , append the new entry to the
history stack. The default value is false .
HistoryEntryState: Gets or sets the state to append to the history entry.

C#

Navigation.NavigateTo("/path", new NavigationOptions


{
HistoryEntryState = "Navigation state"
});

For more information on obtaining the state associated with the target history entry while handling location
changes, see the Handle/prevent location changes section.
Query strings
Use the [SupplyParameterFromQuery] attribute with the [Parameter] attribute to specify that a component
parameter of a routable component can come from the query string.

7 Note

Component parameters can only receive query parameter values in routable components with an @page
directive.

Only routable components directly receive query parameters in order to avoid subverting top-down
information flow and to make parameter processing order clear, both by the framework and by the app. This
design avoids subtle bugs in app code that was written assuming a specific parameter processing order.
You're free to define custom cascading parameters or directly assign to regular component parameters in
order to pass query parameter values to non-routable components.

Component parameters supplied from the query string support the following types:

bool , DateTime , decimal , double , float , Guid , int , long , string .

Nullable variants of the preceding types.


Arrays of the preceding types, whether they're nullable or not nullable.

The correct culture-invariant formatting is applied for the given type (CultureInfo.InvariantCulture).

Specify the [SupplyParameterFromQuery] attribute's Name property to use a query parameter name different from
the component parameter name. In the following example, the C# name of the component parameter is
{COMPONENT PARAMETER NAME} . A different query parameter name is specified for the {QUERY PARAMETER NAME}
placeholder:

C#

[Parameter]
[SupplyParameterFromQuery(Name = "{QUERY PARAMETER NAME}")]
public string? {COMPONENT PARAMETER NAME} { get; set; }

In the following example with a URL of /search?


filter=scifi%20stars&page=3&star=LeVar%20Burton&star=Gary%20Oldman :

The Filter property resolves to scifi stars .


The Page property resolves to 3 .
The Stars array is filled from query parameters named star ( Name = "star" ) and resolves to LeVar Burton
and Gary Oldman .

Pages/Search.razor :

razor

@page "/search"

<h1>Search Example</h1>

<p>Filter: @Filter</p>

<p>Page: @Page</p>

@if (Stars is not null)


{
<p>Assignees:</p>

<ul>
@foreach (var name in Stars)
{
<li>@name</li>
}
</ul>
}

@code {
[Parameter]
[SupplyParameterFromQuery]
public string? Filter { get; set; }

[Parameter]
[SupplyParameterFromQuery]
public int? Page { get; set; }

[Parameter]
[SupplyParameterFromQuery(Name = "star")]
public string[]? Stars { get; set; }
}

Use NavigationManager.GetUriWithQueryParameter to add, change, or remove one or more query parameters on


the current URL:

razor

@inject NavigationManager Navigation

...

Navigation.GetUriWithQueryParameter("{NAME}", {VALUE})

For the preceding example:

The {NAME} placeholder specifies the query parameter name. The {VALUE} placeholder specifies the value as
a supported type. Supported types are listed later in this section.
A string is returned equal to the current URL with a single parameter:
Added if the query parameter name doesn't exist in the current URL.
Updated to the value provided if the query parameter exists in the current URL.
Removed if the type of the provided value is nullable and the value is null .
The correct culture-invariant formatting is applied for the given type (CultureInfo.InvariantCulture).
The query parameter name and value are URL-encoded.
All of the values with the matching query parameter name are replaced if there are multiple instances of the
type.

Call NavigationManager.GetUriWithQueryParameters to create a URI constructed from Uri with multiple


parameters added, updated, or removed. For each value, the framework uses value?.GetType() to determine the
runtime type for each query parameter and selects the correct culture-invariant formatting. The framework throws
an error for unsupported types.

razor

@inject NavigationManager Navigation

...

Navigation.GetUriWithQueryParameters({PARAMETERS})
The {PARAMETERS} placeholder is an IReadOnlyDictionary<string, object> .

Pass a URI string to GetUriWithQueryParameters to generate a new URI from a provided URI with multiple
parameters added, updated, or removed. For each value, the framework uses value?.GetType() to determine the
runtime type for each query parameter and selects the correct culture-invariant formatting. The framework throws
an error for unsupported types. Supported types are listed later in this section.

razor

@inject NavigationManager Navigation

...

Navigation.GetUriWithQueryParameters("{URI}", {PARAMETERS})

The {URI} placeholder is the URI with or without a query string.


The {PARAMETERS} placeholder is an IReadOnlyDictionary<string, object> .

Supported types are identical to supported types for route constraints:

bool

DateTime
decimal

double

float
Guid

int
long

string

Supported types include:

Nullable variants of the preceding types.


Arrays of the preceding types, whether they're nullable or not nullable.

Replace a query parameter value when the parameter exists


C#

Navigation.GetUriWithQueryParameter("full name", "Morena Baccarin")

Current URL Generated URL

scheme://host/?full%20name=David%20Krumholtz&age=42 scheme://host/?full%20name=Morena%20Baccarin&age=42

scheme://host/?fUlL%20nAmE=David%20Krumholtz&AgE=42 scheme://host/?full%20name=Morena%20Baccarin&AgE=42

scheme://host/? scheme://host/?
full%20name=Jewel%20Staite&age=42&full%20name=Summer%20Glau full%20name=Morena%20Baccarin&age=42&full%20name=Morena%20Baccarin

scheme://host/?full%20name=&age=42 scheme://host/?full%20name=Morena%20Baccarin&age=42

scheme://host/?full%20name= scheme://host/?full%20name=Morena%20Baccarin
Append a query parameter and value when the parameter doesn't exist
C#

Navigation.GetUriWithQueryParameter("name", "Morena Baccarin")

Current URL Generated URL

scheme://host/?age=42 scheme://host/?age=42&name=Morena%20Baccarin

scheme://host/ scheme://host/?name=Morena%20Baccarin

scheme://host/? scheme://host/?name=Morena%20Baccarin

Remove a query parameter when the parameter value is null


C#

Navigation.GetUriWithQueryParameter("full name", (string)null)

Current URL Generated URL

scheme://host/?full%20name=David%20Krumholtz&age=42 scheme://host/?age=42

scheme://host/?full%20name=Sally%20Smith&age=42&full%20name=Summer%20Glau scheme://host/?age=42

scheme://host/?full%20name=Sally%20Smith&age=42&FuLl%20NaMe=Summer%20Glau scheme://host/?age=42

scheme://host/?full%20name=&age=42 scheme://host/?age=42

scheme://host/?full%20name= scheme://host/

Add, update, and remove query parameters


In the following example:

name is removed, if present.


age is added with a value of 25 ( int ), if not present. If present, age is updated to a value of 25 .

eye color is added or updated to a value of green .

C#

Navigation.GetUriWithQueryParameters(
new Dictionary<string, object?>
{
["name"] = null,
["age"] = (int?)25,
["eye color"] = "green"
})

Current URL Generated URL

scheme://host/?name=David%20Krumholtz&age=42 scheme://host/?age=25&eye%20color=green

scheme://host/?NaMe=David%20Krumholtz&AgE=42 scheme://host/?age=25&eye%20color=green

scheme://host/?name=David%20Krumholtz&age=42&keepme=true scheme://host/?age=25&keepme=true&eye%20color=green
Current URL Generated URL

scheme://host/?age=42&eye%20color=87 scheme://host/?age=25&eye%20color=green

scheme://host/? scheme://host/?age=25&eye%20color=green

scheme://host/ scheme://host/?age=25&eye%20color=green

Support for enumerable values


In the following example:

full name is added or updated to Morena Baccarin , a single value.


ping parameters are added or replaced with 35 , 16 , 87 and 240 .

C#

Navigation.GetUriWithQueryParameters(
new Dictionary<string, object?>
{
["full name"] = "Morena Baccarin",
["ping"] = new int?[] { 35, 16, null, 87, 240 }
})

Current URL Generated URL

scheme://host/? scheme://host/?
full%20name=David%20Krumholtz&ping=8&ping=300 full%20name=Morena%20Baccarin&ping=35&ping=16&ping=87&ping=240

scheme://host/? scheme://host/?
ping=8&full%20name=David%20Krumholtz&ping=300 ping=35&full%20name=Morena%20Baccarin&ping=16&ping=87&ping=240

scheme://host/? scheme://host/?
ping=8&ping=300&ping=50&ping=68&ping=42 ping=35&ping=16&ping=87&ping=240&full%20name=Morena%20Baccarin

Navigate with an added or modified query string


To navigate with an added or modified query string, pass a generated URL to NavigateTo.

The following example calls:

GetUriWithQueryParameter to add or replace the name query parameter using a value of Morena Baccarin .
Calls NavigateTo to trigger navigation to the new URL.

C#

Navigation.NavigateTo(
Navigation.GetUriWithQueryParameter("name", "Morena Baccarin"));

User interaction with <Navigating> content


The Router component can indicate to the user that a page transition is occurring.

At the top of the App component ( App.razor ), add an @using directive for the
Microsoft.AspNetCore.Components.Routing namespace:
razor

@using Microsoft.AspNetCore.Components.Routing

Add a <Navigating> tag to the component with markup to display during page transition events. For more
information, see Navigating (API documentation).

In the router element content ( <Router>...</Router> ) of the App component ( App.razor ):

razor

<Navigating>
<p>Loading the requested page&hellip;</p>
</Navigating>

For an example that uses the Navigating property, see Lazy load assemblies in ASP.NET Core Blazor WebAssembly.

Handle asynchronous navigation events with OnNavigateAsync


The Router component supports an OnNavigateAsync feature. The OnNavigateAsync handler is invoked when the
user:

Visits a route for the first time by navigating to it directly in their browser.
Navigates to a new route using a link or a NavigationManager.NavigateTo invocation.

In the App component ( App.razor ):

razor

<Router AppAssembly="@typeof(App).Assembly"
OnNavigateAsync="@OnNavigateAsync">
...
</Router>

@code {
private async Task OnNavigateAsync(NavigationContext args)
{
...
}
}

For an example that uses OnNavigateAsync, see Lazy load assemblies in ASP.NET Core Blazor WebAssembly.

When prerendering on the server in a Blazor Server app or hosted Blazor WebAssembly app, OnNavigateAsync is
executed twice:

Once when the requested endpoint component is initially rendered statically.


A second time when the browser renders the endpoint component.

To prevent developer code in OnNavigateAsync from executing twice, the App component can store the
NavigationContext for use in OnAfterRender{Async}, where firstRender can be checked. For more information, see
Prerendering with JavaScript interop in the Blazor Lifecycle article.

Handle cancellations in OnNavigateAsync


The NavigationContext object passed to the OnNavigateAsync callback contains a CancellationToken that's set
when a new navigation event occurs. The OnNavigateAsync callback must throw when this cancellation token is set
to avoid continuing to run the OnNavigateAsync callback on an outdated navigation.

If a user navigates to an endpoint but then immediately navigates to a new endpoint, the app shouldn't continue
running the OnNavigateAsync callback for the first endpoint.

In the following App component example:

The cancellation token is passed in the call to PostAsJsonAsync , which can cancel the POST if the user
navigates away from the /about endpoint.
The cancellation token is set during a product prefetch operation if the user navigates away from the /store
endpoint.

App.razor :

razor

@inject HttpClient Http


@inject ProductCatalog Products

<Router AppAssembly="@typeof(App).Assembly"
OnNavigateAsync="@OnNavigateAsync">
...
</Router>

@code {
private async Task OnNavigateAsync(NavigationContext context)
{
if (context.Path == "/about")
{
var stats = new Stats { Page = "/about" };
await Http.PostAsJsonAsync("api/visited", stats,
context.CancellationToken);
}
else if (context.Path == "/store")
{
var productIds = new[] { 345, 789, 135, 689 };

foreach (var productId in productIds)


{
context.CancellationToken.ThrowIfCancellationRequested();
Products.Prefetch(productId);
}
}
}
}

7 Note

Not throwing if the cancellation token in NavigationContext is canceled can result in unintended behavior,
such as rendering a component from a previous navigation.

Handle/prevent location changes


RegisterLocationChangingHandler registers a handler to process incoming navigation events. The handler's
context provided by LocationChangingContext includes the following properties:

TargetLocation: Gets the target location.


HistoryEntryState: Gets the state associated with the target history entry.
IsNavigationIntercepted: Gets whether the navigation was intercepted from a link.
CancellationToken: Gets a CancellationToken to determine if the navigation was canceled, for example, to
determine if the user triggered a different navigation.
PreventNavigation: Called to prevent the navigation from continuing.

A component can register multiple location changing handlers in its OnAfterRender or OnAfterRenderAsync
methods. Navigation invokes all of the location changing handlers registered across the entire app (across multiple
components), and any internal navigation executes them all in parallel. In addition to NavigateTo handlers are
invoked:

When selecting internal links, which are links that point to URLs under the app's base path.
When navigating using the forward and back buttons in a browser.

Handlers are only executed for internal navigation within the app. If the user selects a link that navigates to a
different site or changes the address bar to a different site manually, location changing handlers aren't executed.

Implement IDisposable and dispose registered handlers to unregister them. For more information, see ASP.NET
Core Razor component lifecycle.

) Important

Don't attempt to execute Document Object Model (DOM) cleanup tasks via JavaScript (JS) interop when
handling location changes. Use the MutationObserver pattern in JS on the client. For more information, see
Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

In the following example, a location changing handler is registered for navigation events.

Pages/NavHandler.razor :

razor

@page "/nav-handler"
@inject NavigationManager Navigation
@implements IDisposable

<p>
<button @onclick="@(() => Navigation.NavigateTo("/"))">
Home (Allowed)
</button>
<button @onclick="@(() => Navigation.NavigateTo("/counter"))">
Counter (Prevented)
</button>
</p>

@code {
private IDisposable? registration;

protected override void OnAfterRender(bool firstRender)


{
if (firstRender)
{
registration =
Navigation.RegisterLocationChangingHandler(OnLocationChanging);
}
}

private ValueTask OnLocationChanging(LocationChangingContext context)


{
if (context.TargetLocation == "/counter")
{
context.PreventNavigation();
}

return ValueTask.CompletedTask;
}

public void Dispose() => registration?.Dispose();


}

Since internal navigation can be canceled asynchronously, multiple overlapping calls to registered handlers may
occur. For example, multiple handler calls may occur when the user rapidly selects the back button on a page or
selects multiple links before a navigation is executed. The following is a summary of the asynchronous navigation
logic:

If any location changing handlers are registered, all navigation is initially reverted, then replayed if the
navigation isn't canceled.
If overlapping navigation requests are made, the latest request always cancels earlier requests, which means
the following:
The app may treat multiple back and forward button selections as a single selection.
If the user selects multiple links before the navigation completes, the last link selected determines the
navigation.

For more information on passing NavigationOptions to NavigateTo to control entries and state of the navigation
history stack, see the Navigation options section.

For additional example code, see the NavigationManagerComponent in the BasicTestApp (dotnet/aspnetcore
reference source) .

7 Note

Documentation links to .NET reference source usually load the repository's default branch, which represents
the current development for the next release of .NET. To select a tag for a specific release, use the Switch
branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core
source code (dotnet/AspNetCore.Docs #26205) .

The NavigationLock component intercepts navigation events as long as it is rendered, effectively "locking" any
given navigation until a decision is made to either proceed or cancel. Use NavigationLock when navigation
interception can be scoped to the lifetime of a component.

NavigationLock parameters:

ConfirmExternalNavigation sets a browser dialog to prompt the user to either confirm or cancel external
navigation. The default value is false . Displaying the confirmation dialog requires initial user interaction with
the page before triggering external navigation with the URL in the browser's address bar. For more
information on the interaction requirement, see Window: beforeunload event (MDN documentation) .
OnBeforeInternalNavigation sets a callback for internal navigation events.

In the following NavLock component:

An attempt to follow the link to Microsoft's website must be confirmed by the user before the navigation to
https://www.microsoft.com succeeds.

PreventNavigation is called to prevent navigation from occurring if the user declines to confirm the
navigation via a JavaScript (JS) interop call that spawns the JS confirm dialog .

Pages/NavLock.razor :
razor

@page "/nav-lock"
@inject IJSRuntime JSRuntime
@inject NavigationManager Navigation

<NavigationLock ConfirmExternalNavigation="true"
OnBeforeInternalNavigation="OnBeforeInternalNavigation" />

<p>
<button @onclick="Navigate">Navigate</button>
</p>

<p>
<a href="https://www.microsoft.com">Microsoft homepage</a>
</p>

@code {
private void Navigate()
{
Navigation.NavigateTo("/");
}

private async Task OnBeforeInternalNavigation(LocationChangingContext context)


{
var isConfirmed = await JSRuntime.InvokeAsync<bool>("confirm",
"Are you sure you want to navigate to the Index page?");

if (!isConfirmed)
{
context.PreventNavigation();
}
}
}

For additional example code, see the ConfigurableNavigationLock component in the BasicTestApp
(dotnet/aspnetcore reference source) .

NavLink and NavMenu components


Use a NavLink component in place of HTML hyperlink elements ( <a> ) when creating navigation links. A NavLink
component behaves like an <a> element, except it toggles an active CSS class based on whether its href
matches the current URL. The active class helps a user understand which page is the active page among the
navigation links displayed. Optionally, assign a CSS class name to NavLink.ActiveClass to apply a custom CSS class
to the rendered link when the current route matches the href .

7 Note

The NavMenu component ( NavMenu.razor ) is provided in the Shared folder of an app generated from the
Blazor project templates.

There are two NavLinkMatch options that you can assign to the Match attribute of the <NavLink> element:

NavLinkMatch.All: The NavLink is active when it matches the entire current URL.
NavLinkMatch.Prefix (default): The NavLink is active when it matches any prefix of the current URL.

In the preceding example, the Home NavLink href="" matches the home URL and only receives the active CSS
class at the app's default base path ( / ). The second NavLink receives the active class when the user visits any URL
with a component prefix (for example, /component and /component/another-segment ).
Additional NavLink component attributes are passed through to the rendered anchor tag. In the following
example, the NavLink component includes the target attribute:

razor

<NavLink href="example-page" target="_blank">Example page</NavLink>

The following HTML markup is rendered:

HTML

<a href="example-page" target="_blank">Example page</a>

2 Warning

Due to the way that Blazor renders child content, rendering NavLink components inside a for loop requires a
local index variable if the incrementing loop variable is used in the NavLink (child) component's content:

razor

@for (int c = 0; c < 10; c++)


{
var current = c;
<li ...>
<NavLink ... href="@c">
<span ...></span> @current
</NavLink>
</li>
}

Using an index variable in this scenario is a requirement for any child component that uses a loop variable in
its child content, not just the NavLink component.

Alternatively, use a foreach loop with Enumerable.Range:

razor

@foreach (var c in Enumerable.Range(0,10))


{
<li ...>
<NavLink ... href="@c">
<span ...></span> @c
</NavLink>
</li>
}

ASP.NET Core endpoint routing integration


This section only applies to Blazor Server apps.

Blazor Server is integrated into ASP.NET Core Endpoint Routing. An ASP.NET Core app is configured to accept
incoming connections for interactive components with MapBlazorHub in Program.cs :

C#
app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

The typical configuration is to route all requests to a Razor page, which acts as the host for the server-side part of
the Blazor Server app. By convention, the host page is usually named _Host.cshtml in the Pages folder of the app.

The route specified in the host file is called a fallback route because it operates with a low priority in route
matching. The fallback route is used when other routes don't match. This allows the app to use other controllers
and pages without interfering with component routing in the Blazor Server app.

For information on configuring MapFallbackToPage for non-root URL server hosting, see Host and deploy ASP.NET
Core Blazor.
ASP.NET Core Blazor configuration
Article • 04/04/2023

This article explains configuration of Blazor apps, including app settings, authentication,
and logging configuration.

) Important

This topic applies to Blazor WebAssembly. For general guidance on ASP.NET Core
app configuration, see Configuration in ASP.NET Core.

Blazor WebAssembly loads configuration from the following app settings files by
default:

wwwroot/appsettings.json .
wwwroot/appsettings.{ENVIRONMENT}.json , where the {ENVIRONMENT} placeholder is

the app's runtime environment.

7 Note

Logging configuration placed into an app settings file in wwwroot of a Blazor


WebAssembly app isn't loaded by default. For for information, see the Logging
configuration section later in this article.

Other configuration providers registered by the app can also provide configuration, but
not all providers or provider features are appropriate for Blazor WebAssembly apps:

Azure Key Vault configuration provider: The provider isn't supported for managed
identity and application ID (client ID) with client secret scenarios. Application ID
with a client secret isn't recommended for any ASP.NET Core app, especially Blazor
WebAssembly apps because the client secret can't be secured client-side to access
the Azure Key Vault service.
Azure App configuration provider: The provider isn't appropriate for Blazor
WebAssembly apps because Blazor WebAssembly apps don't run on a server in
Azure.

2 Warning

Configuration and settings files in a Blazor WebAssembly app are visible to users.
Don't store app secrets, credentials, or any other sensitive data in the
configuration or files of a Blazor WebAssembly app.

For more information on configuration providers, see Configuration in ASP.NET Core.

App settings configuration


Configuration in app settings files are loaded by default. In the following example, a UI
configuration value is stored in an app settings file and loaded by the Blazor framework
automatically. The value is read by a component.

wwwroot/appsettings.json :

JSON

{
"h1FontSize": "50px"
}

Inject an IConfiguration instance into a component to access the configuration data.

Pages/ConfigurationExample.razor :

razor

@page "/configuration-example"
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<h1 style="font-size:@Configuration["h1FontSize"]">
Configuration example
</h1>

Client security restrictions prevent direct access to files, including settings files for app
configuration. To read configuration files in addition to appsettings.json / appsettings.
{ENVIRONMENT}.json from the wwwroot folder into configuration, use an HttpClient.

2 Warning

Configuration and settings files in a Blazor WebAssembly app are visible to users.
Don't store app secrets, credentials, or any other sensitive data in the
configuration or files of a Blazor WebAssembly app.
The following example reads a configuration file ( cars.json ) into the app's
configuration.

wwwroot/cars.json :

JSON

{
"size": "tiny"
}

Add the namespace for Microsoft.Extensions.Configuration to Program.cs :

C#

using Microsoft.Extensions.Configuration;

In Program.cs , modify the existing HttpClient service registration to use the client to
read the file:

C#

var http = new HttpClient()


{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
};

builder.Services.AddScoped(sp => http);

using var response = await http.GetAsync("cars.json");


using var stream = await response.Content.ReadAsStreamAsync();

builder.Configuration.AddJsonStream(stream);

Memory Configuration Source


The following example uses a MemoryConfigurationSource in Program.cs to supply
additional configuration.

Add the namespace for Microsoft.Extensions.Configuration.Memory to Program.cs :

C#

using Microsoft.Extensions.Configuration.Memory;
In Program.cs :

C#

var vehicleData = new Dictionary<string, string>()


{
{ "color", "blue" },
{ "type", "car" },
{ "wheels:count", "3" },
{ "wheels:brand", "Blazin" },
{ "wheels:brand:type", "rally" },
{ "wheels:year", "2008" },
};

var memoryConfig = new MemoryConfigurationSource { InitialData = vehicleData


};

builder.Configuration.Add(memoryConfig);

Inject an IConfiguration instance into a component to access the configuration data.

Pages/MemoryConfig.razor :

razor

@page "/memory-config"
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<h1>Memory configuration example</h1>

<h2>General specifications</h2>

<ul>
<li>Color: @Configuration["color"]</li>
<li>Type: @Configuration["type"]</li>
</ul>

<h2>Wheels</h2>

<ul>
<li>Count: @Configuration["wheels:count"]</li>
<li>Brand: @Configuration["wheels:brand"]</li>
<li>Type: @Configuration["wheels:brand:type"]</li>
<li>Year: @Configuration["wheels:year"]</li>
</ul>

Obtain a section of the configuration in C# code with IConfiguration.GetSection. The


following example obtains the wheels section for the configuration in the preceding
example:
razor

@code {
protected override void OnInitialized()
{
var wheelsSection = Configuration.GetSection("wheels");

...
}
}

Authentication configuration
Provide authentication configuration in an app settings file.

wwwroot/appsettings.json :

JSON

{
"Local": {
"Authority": "{AUTHORITY}",
"ClientId": "{CLIENT ID}"
}
}

Load the configuration for an Identity provider with ConfigurationBinder.Bind in


Program.cs . The following example loads configuration for an OIDC provider.

Program.cs :

C#

builder.Services.AddOidcAuthentication(options =>
builder.Configuration.Bind("Local", options.ProviderOptions));

Logging configuration
This section applies to Blazor WebAssembly apps that configure logging via an app
settings file in the wwwroot folder.

Add the Microsoft.Extensions.Logging.Configuration package to the app.

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 .

In the app settings file, provide logging configuration. The logging configuration is
loaded in Program.cs .

wwwroot/appsettings.json :

JSON

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

In Program.cs :

C#

builder.Logging.AddConfiguration(
builder.Configuration.GetSection("Logging"));

Host builder configuration


Read host builder configuration from WebAssemblyHostBuilder.Configuration in
Program.cs .

In Program.cs :

C#

var hostname = builder.Configuration["HostName"];

Cached configuration
Configuration files are cached for offline use. With Progressive Web Applications (PWAs),
you can only update configuration files when creating a new deployment. Editing
configuration files between deployments has no effect because:

Users have cached versions of the files that they continue to use.
The PWA's service-worker.js and service-worker-assets.js files must be rebuilt
on compilation, which signal to the app on the user's next online visit that the app
has been redeployed.

For more information on how background updates are handled by PWAs, see ASP.NET
Core Blazor Progressive Web Application (PWA).

Options configuration
Options configuration for Blazor WebAssembly apps requires adding a package
reference for the Microsoft.Extensions.Options.ConfigurationExtensions NuGet
package.

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 .

Example:

C#

builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));

Not all of the ASP.NET Core Options features are supported in Razor components. For
example, IOptionsSnapshot<TOptions> and IOptionsMonitor<TOptions> configuration
is supported, but recomputing option values for these interfaces isn't supported outside
of reloading the app by either requesting the app in a new browser tab or selecting the
browser's reload button. Merely calling StateHasChanged doesn't update snapshot or
monitored option values when the underlying configuration changes.
ASP.NET Core Blazor dependency
injection
Article • 04/04/2023

By Rainer Stropek and Mike Rousos

This article explains how Blazor apps can inject services into components.

Dependency injection (DI) is a technique for accessing services configured in a central


location:

Framework-registered services can be injected directly into components of Blazor


apps.
Blazor apps define and register custom services and make them available
throughout the app via DI.

7 Note

We recommend reading Dependency injection in ASP.NET Core before reading


this topic.

Default services
The services shown in the following table are commonly used in Blazor apps.

Service Lifetime Description


Service Lifetime Description

HttpClient Scoped Provides methods for sending HTTP requests and


receiving HTTP responses from a resource
identified by a URI.

The instance of HttpClient in a Blazor


WebAssembly app is registered by the app in
Program.cs and uses the browser for handling the
HTTP traffic in the background.

Blazor Server apps don't include an HttpClient


configured as a service by default. Provide an
HttpClient to a Blazor Server app.

For more information, see Call a web API from an


ASP.NET Core Blazor app.

An HttpClient is registered as a scoped service, not


singleton. For more information, see the Service
lifetime section.

IJSRuntime Blazor WebAssembly: Represents an instance of a JavaScript runtime


Singleton where JavaScript calls are dispatched. For more
information, see Call JavaScript functions from
Blazor Server: .NET methods in ASP.NET Core Blazor.
Scoped
When seeking to inject the service into a singleton
The Blazor framework service in Blazor Server apps, take either of the
registers IJSRuntime following approaches:
in the app's service
container. Change the service registration to scoped to
match IJSRuntime's registration, which is
appropriate if the service deals with user-
specific state.
Pass the IJSRuntime into the singleton
service's implementation as an argument of
its method calls instead of injecting it into
the singleton.
Service Lifetime Description

NavigationManager Blazor WebAssembly: Contains helpers for working with URIs and
Singleton navigation state. For more information, see URI
and navigation state helpers.
Blazor Server:
Scoped

The Blazor framework


registers
NavigationManager
in the app's service
container.

Additional services registered by the Blazor framework are described in the


documentation where they're used to describe Blazor features, such as configuration
and logging.

A custom service provider doesn't automatically provide the default services listed in the
table. If you use a custom service provider and require any of the services shown in the
table, add the required services to the new service provider.

Add services to a Blazor WebAssembly app


Configure services for the app's service collection in Program.cs . In the following
example, the ExampleDependency implementation is registered for IExampleDependency :

C#

var builder = WebAssemblyHostBuilder.CreateDefault(args);


...
builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
...

await builder.Build().RunAsync();

After the host is built, services are available from the root DI scope before any
components are rendered. This can be useful for running initialization logic before
rendering content:

C#

var builder = WebAssemblyHostBuilder.CreateDefault(args);


...
builder.Services.AddSingleton<WeatherService>();
...
var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();


await weatherService.InitializeWeatherAsync();

await host.RunAsync();

The host provides a central configuration instance for the app. Building on the
preceding example, the weather service's URL is passed from a default configuration
source (for example, appsettings.json ) to InitializeWeatherAsync :

C#

var builder = WebAssemblyHostBuilder.CreateDefault(args);


...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();


await weatherService.InitializeWeatherAsync(
host.Configuration["WeatherServiceUrl"]);

await host.RunAsync();

Add services to a Blazor Server app


After creating a new app, examine part of the Program.cs file:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

The builder variable represents a Microsoft.AspNetCore.Builder.WebApplicationBuilder


with an IServiceCollection, which is a list of service descriptor objects. Services are added
by providing service descriptors to the service collection. The following example
demonstrates the concept with the IDataAccess interface and its concrete
implementation DataAccess :

C#
builder.Services.AddSingleton<IDataAccess, DataAccess>();

Register common services in a hosted Blazor


WebAssembly solution
If one or more common services are required by the Server and Client projects of a
hosted Blazor WebAssembly solution, you can place the common service registrations in
a method in the Client project and call the method to register the services in both
projects.

First, factor common service registrations into a separate method. For example, create a
ConfigureCommonServices method in the Client project:

C#

public static void ConfigureCommonServices(IServiceCollection services)


{
services.Add...;
}

In the Client project's Program.cs file, call ConfigureCommonServices to register the


common services:

C#

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

In the Server project's Program.cs file, call ConfigureCommonServices to register the


common services for the Server project:

C#

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);
For an example of this approach, see ASP.NET Core Blazor WebAssembly additional
security scenarios.

Service lifetime
Services can be configured with the lifetimes shown in the following table.

Lifetime Description

Scoped Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped -
registered services behave like Singleton services.

The Blazor Server hosting model supports the Scoped lifetime across HTTP requests
but not across SignalR connection/circuit messages among components that are
loaded on the client. The Razor Pages or MVC portion of the app treats scoped
services normally and recreates the services on each HTTP request when navigating
among pages or views or from a page or view to a component. Scoped services aren't
reconstructed when navigating among components on the client, where the
communication to the server takes place over the SignalR connection of the user's
circuit, not via HTTP requests. In the following component scenarios on the client,
scoped services are reconstructed because a new circuit is created for the user:

The user closes the browser's window. The user opens a new window and
navigates back to the app.
The user closes a tab of the app in a browser window. The user opens a new tab
and navigates back to the app.
The user selects the browser's reload/refresh button.

For more information on preserving user state across scoped services in Blazor Server
apps, see ASP.NET Core Blazor hosting models.

Singleton DI creates a single instance of the service. All components requiring a Singleton
service receive the same instance of the service.

Transient Whenever a component obtains an instance of a Transient service from the service
container, it receives a new instance of the service.

The DI system is based on the DI system in ASP.NET Core. For more information, see
Dependency injection in ASP.NET Core.

Request a service in a component


After services are added to the service collection, inject the services into the
components using the @inject Razor directive, which has two parameters:

Type: The type of the service to inject.


Property: The name of the property receiving the injected app service. The
property doesn't require manual creation. The compiler creates the property.

For more information, see Dependency injection into views in ASP.NET Core.

Use multiple @inject statements to inject different services.

The following example shows how to use @inject. The service implementing
Services.IDataAccess is injected into the component's property DataRepository . Note

how the code is only using the IDataAccess abstraction:

razor

@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)


{
<ul>
@foreach (var customer in customers)
{
<li>@customer.FirstName @customer.LastName</li>
}
</ul>
}

@code {
private IReadOnlyList<Customer>? customers;

protected override async Task OnInitializedAsync()


{
customers = await DataRepository.GetAllCustomersAsync();
}

private class Customer


{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}

private interface IDataAccess


{
public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
}
}

Internally, the generated property ( DataRepository ) uses the [Inject] attribute. Typically,
this attribute isn't used directly. If a base class is required for components and injected
properties are also required for the base class, manually add the [Inject] attribute:
C#

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent


{
[Inject]
protected IDataAccess DataRepository { get; set; }

...
}

7 Note

Since injected services are expected to be available, don't mark injected services as
nullable. Instead, assign a default literal with the null-forgiving operator ( default! ).
For example:

C#

[Inject]
private IExampleService ExampleService { get; set; } = default!;

For more information, see the following resources:

Nullable reference types (NRTs) and .NET compiler null-state static analysis
Nullable reference types (C# guide)
default value expressions (C# reference)
! (null-forgiving) operator (C# reference)

In components derived from the base class, the @inject directive isn't required. The
InjectAttribute of the base class is sufficient:

razor

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

Use DI in services
Complex services might require additional services. In the following example,
DataAccess requires the HttpClient default service. @inject (or the [Inject] attribute) isn't
available for use in services. Constructor injection must be used instead. Required
services are added by adding parameters to the service's constructor. When DI creates
the service, it recognizes the services it requires in the constructor and provides them
accordingly. In the following example, the constructor receives an HttpClient via DI.
HttpClient is a default service.

C#

using System.Net.Http;

public class DataAccess : IDataAccess


{
public DataAccess(HttpClient http)
{
...
}
}

Prerequisites for constructor injection:

One constructor must exist whose arguments can all be fulfilled by DI. Additional
parameters not covered by DI are allowed if they specify default values.
The applicable constructor must be public .
One applicable constructor must exist. In case of an ambiguity, DI throws an
exception.

Utility base component classes to manage a DI


scope
In ASP.NET Core apps, scoped services are typically scoped to the current request. After
the request completes, any scoped or transient services are disposed by the DI system.
In Blazor Server apps, the request scope lasts for the duration of the client connection,
which can result in transient and scoped services living much longer than expected. In
Blazor WebAssembly apps, services registered with a scoped lifetime are treated as
singletons, so they live longer than scoped services in typical ASP.NET Core apps.

7 Note

To detect disposable transient services in an app, see the following sections:


Detect transient disposables in Blazor WebAssembly apps Detect transient
disposables in Blazor Server apps

An approach that limits a service lifetime in Blazor apps is use of the


OwningComponentBase type. OwningComponentBase is an abstract type derived from
ComponentBase that creates a DI scope corresponding to the lifetime of the
component. Using this scope, it's possible to use DI services with a scoped lifetime and
have them live as long as the component. When the component is destroyed, services
from the component's scoped service provider are disposed as well. This can be useful
for services that:

Should be reused within a component, as the transient lifetime is inappropriate.


Shouldn't be shared across components, as the singleton lifetime is inappropriate.

Two versions of OwningComponentBase type are available and described in the next
two sections:

OwningComponentBase
OwningComponentBase<TService>

OwningComponentBase

OwningComponentBase is an abstract, disposable child of the ComponentBase type


with a protected ScopedServices property of type IServiceProvider. The provider can be
used to resolve services that are scoped to the lifetime of the component.

DI services injected into the component using @inject or the [Inject] attribute aren't
created in the component's scope. To use the component's scope, services must be
resolved using ScopedServices with either GetRequiredService or GetService. Any
services resolved using the ScopedServices provider have their dependencies provided
in the component's scope.

The following example demonstrates the difference between injecting a scoped service
directly and resolving a service using ScopedServices in a Blazor Server app. The
following interface and implementation for a time travel class include a DT property to
hold a DateTime value. The implementation calls DateTime.Now to set DT when the
TimeTravel class is instantiated.

ITimeTravel.cs :

C#
public interface ITimeTravel
{
public DateTime DT { get; set; }
}

TimeTravel.cs :

C#

public class TimeTravel : ITimeTravel


{
public DateTime DT { get; set; } = DateTime.Now;
}

The service is registered as scoped in Program.cs of a Blazor Server app. In a Blazor


Server app, scoped services have a lifetime equal to the duration of the client
connection, known as a circuit.

In Program.cs :

C#

builder.Services.AddScoped<ITimeTravel, TimeTravel>();

In the following TimeTravel component:

The time travel service is directly injected with @inject as TimeTravel1 .


The service is also resolved separately with ScopedServices and
GetRequiredService as TimeTravel2 .

Pages/TimeTravel.razor :

razor

@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
<li>TimeTravel1.DT: @TimeTravel1?.DT</li>
<li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
private ITimeTravel? TimeTravel2 { get; set; }

protected override void OnInitialized()


{
TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
}
}

If you're placing this example into a test app, add the TimeTravel component to the
NavMenu component.

In Shared/NavMenu.razor :

razor

<div class="nav-item px-3">


<NavLink class="nav-link" href="time-travel">
<span class="oi oi-list-rich" aria-hidden="true"></span> Time travel
</NavLink>
</div>

Initially navigating to the TimeTravel component, the time travel service is instantiated
twice when the component loads, and TimeTravel1 and TimeTravel2 have the same
initial value:

TimeTravel1.DT: 8/31/2022 2:54:45 PM


TimeTravel2.DT: 8/31/2022 2:54:45 PM

When navigating away from the TimeTravel component to another component and
back to the TimeTravel component:

TimeTravel1 is provided the same service instance that was created when the

component first loaded, so the value of DT remains the same.


TimeTravel2 obtains a new ITimeTravel service instance in TimeTravel2 with a

new DT value.

TimeTravel1.DT: 8/31/2022 2:54:45 PM


TimeTravel2.DT: 8/31/2022 2:54:48 PM

TimeTravel1 is tied to the user's circuit, which remains intact and isn't disposed until the
underlying circuit is deconstructed. For example, the service is disposed if the circuit is
disconnected for the disconnected circuit retention period.
In spite of the scoped service registration in Program.cs and the longevity of the user's
circuit, TimeTravel2 receives a new ITimeTravel service instance each time the
component is initialized.

OwningComponentBase<TService>

OwningComponentBase<TService> derives from OwningComponentBase and adds a


Service property that returns an instance of T from the scoped DI provider. This type is
a convenient way to access scoped services without using an instance of
IServiceProvider when there's one primary service the app requires from the DI
container using the component's scope. The ScopedServices property is available, so the
app can get services of other types, if necessary.

razor

@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>

<h1>Users (@Service.Users.Count())</h1>

<ul>
@foreach (var user in Service.Users)
{
<li>@user.UserName</li>
}
</ul>

Use of an Entity Framework Core (EF Core)


DbContext from DI
For more information, see ASP.NET Core Blazor Server with Entity Framework Core (EF
Core).

Detect transient disposables in Blazor


WebAssembly apps
The following example shows how to detect disposable transient services in an app that
should use OwningComponentBase. For more information, see the Utility base
component classes to manage a DI scope section.

DetectIncorrectUsagesOfTransientDisposables.cs for Blazor WebAssembly apps:


C#

using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
using BlazorWebAssemblyTransientDisposable;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

public static class WebHostBuilderTransientDisposableExtensions


{
public static WebAssemblyHostBuilder
DetectIncorrectUsageOfTransients(
this WebAssemblyHostBuilder builder)
{
builder
.ConfigureContainer(
new
DetectIncorrectUsageOfTransientDisposablesServiceFactory());

return builder;
}

public static WebAssemblyHost EnableTransientDisposableDetection(


this WebAssemblyHost webAssemblyHost)
{
webAssemblyHost.Services
.GetRequiredService<ThrowOnTransientDisposable>
().ShouldThrow = true;

return webAssemblyHost;
}
}
}

namespace BlazorWebAssemblyTransientDisposable
{
public class DetectIncorrectUsageOfTransientDisposablesServiceFactory
: IServiceProviderFactory<IServiceCollection>
{
public IServiceCollection CreateBuilder(IServiceCollection services)
=>
services;

public IServiceProvider CreateServiceProvider(


IServiceCollection containerBuilder)
{
var collection = new ServiceCollection();

foreach (var descriptor in containerBuilder)


{
if (descriptor.Lifetime == ServiceLifetime.Transient &&
descriptor.ImplementationType != null &&
typeof(IDisposable).IsAssignableFrom(
descriptor.ImplementationType))
{
collection.Add(CreatePatchedDescriptor(descriptor));
}
else if (descriptor.Lifetime == ServiceLifetime.Transient &&
descriptor.ImplementationFactory != null)
{

collection.Add(CreatePatchedFactoryDescriptor(descriptor));
}
else
{
collection.Add(descriptor);
}
}

collection.AddScoped<ThrowOnTransientDisposable>();

return collection.BuildServiceProvider();
}

private ServiceDescriptor CreatePatchedFactoryDescriptor(


ServiceDescriptor original)
{
var newDescriptor = new ServiceDescriptor(
original.ServiceType,
(sp) =>
{
var originalFactory = original.ImplementationFactory;

if (originalFactory is null)
{
throw new InvalidOperationException(
"originalFactory is null.");
}

var originalResult = originalFactory(sp);

var throwOnTransientDisposable =
sp.GetRequiredService<ThrowOnTransientDisposable>();
if (throwOnTransientDisposable.ShouldThrow &&
originalResult is IDisposable d)
{
throw new InvalidOperationException("Trying to
resolve " +
$"transient disposable service
{d.GetType().Name} in " +
"the wrong scope. Use an
'OwningComponentBase<T>' " +
"component base class for the service 'T' you
are " +
"trying to resolve.");
}

return originalResult;
},
original.Lifetime);

return newDescriptor;
}

private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor


original)
{
var newDescriptor = new ServiceDescriptor(
original.ServiceType,
(sp) => {
var throwOnTransientDisposable =
sp.GetRequiredService<ThrowOnTransientDisposable>();
if (throwOnTransientDisposable.ShouldThrow)
{
throw new InvalidOperationException("Trying to
resolve " +
"transient disposable service " +
$"{original.ImplementationType?.Name} in the wrong "
+
"scope. Use an 'OwningComponentBase<T>' component
base " +
"class for the service 'T' you are trying to
resolve.");
}

if (original.ImplementationType is null)
{
throw new InvalidOperationException(
"ImplementationType is null.");
}

return ActivatorUtilities.CreateInstance(sp,
original.ImplementationType);
},
ServiceLifetime.Transient);

return newDescriptor;
}
}

internal class ThrowOnTransientDisposable


{
public bool ShouldThrow { get; set; }
}
}

TransientDisposable.cs :

C#
public class TransientDisposable : IDisposable
{
public void Dispose() => throw new NotImplementedException();
}

The TransientDisposable in the following example is detected.

Program.cs :

C#

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWebAssemblyTransientDisposable;

var builder = WebAssemblyHostBuilder.CreateDefault(args);


builder.DetectIncorrectUsageOfTransients();
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped(sp =>
new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});

var host = builder.Build();


host.EnableTransientDisposableDetection();
await host.RunAsync();

The app can register transient disposables without throwing an exception. However,
attempting to resolve a transient disposable results in an InvalidOperationException, as
the following example shows.

Pages/TransientExample.razor :

razor

@page "/transient-example"
@inject TransientDisposable TransientDisposable

<h1>Transient Disposable Detection</h1>

Navigate to the TransientExample component at /transient-example and an


InvalidOperationException is thrown when the framework attempts to construct an
instance of TransientDisposable :
System.InvalidOperationException: Trying to resolve transient disposable service
TransientDisposable in the wrong scope. Use an 'OwningComponentBase<T>'
component base class for the service 'T' you are trying to resolve.

7 Note

Transient service registrations for IHttpClientFactory handlers are recommended.


The TransientExample component in this section indicates the following transient
disposables in Blazor WebAssembly apps that use authentication, which is
expected:

BaseAddressAuthorizationMessageHandler
AuthorizationMessageHandler

Detect transient disposables in Blazor Server


apps
The following example shows how to detect disposable transient services in an app that
should use OwningComponentBase. For more information, see the Utility base
component classes to manage a DI scope section.

DetectIncorrectUsagesOfTransientDisposables.cs :

C#

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
using BlazorServerTransientDisposable;

public static class WebHostBuilderTransientDisposableExtensions


{
public static WebApplicationBuilder
DetectIncorrectUsageOfTransients(
this WebApplicationBuilder builder)
{
builder.Host
.UseServiceProviderFactory(
new
DetectIncorrectUsageOfTransientDisposablesServiceFactory())
.ConfigureServices(
s =>
s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
ThrowOnTransientDisposableHandler>()));

return builder;
}
}
}

namespace BlazorServerTransientDisposable
{
internal class ThrowOnTransientDisposableHandler : CircuitHandler
{
public ThrowOnTransientDisposableHandler(
ThrowOnTransientDisposable throwOnTransientDisposable)
{
throwOnTransientDisposable.ShouldThrow = true;
}
}

public class DetectIncorrectUsageOfTransientDisposablesServiceFactory


: IServiceProviderFactory<IServiceCollection>
{
public IServiceCollection CreateBuilder(IServiceCollection services)
=>
services;

public IServiceProvider CreateServiceProvider(


IServiceCollection containerBuilder)
{
var collection = new ServiceCollection();

foreach (var descriptor in containerBuilder)


{
if (descriptor.Lifetime == ServiceLifetime.Transient &&
descriptor.ImplementationType != null &&
typeof(IDisposable).IsAssignableFrom(
descriptor.ImplementationType))
{
collection.Add(CreatePatchedDescriptor(descriptor));
}
else if (descriptor.Lifetime == ServiceLifetime.Transient &&
descriptor.ImplementationFactory != null)
{

collection.Add(CreatePatchedFactoryDescriptor(descriptor));
}
else
{
collection.Add(descriptor);
}
}

collection.AddScoped<ThrowOnTransientDisposable>();

return collection.BuildServiceProvider();
}

private ServiceDescriptor CreatePatchedFactoryDescriptor(


ServiceDescriptor original)
{
var newDescriptor = new ServiceDescriptor(
original.ServiceType,
(sp) =>
{
var originalFactory = original.ImplementationFactory;

if (originalFactory is null)
{
throw new InvalidOperationException(
"originalFactory is null.");
}

var originalResult = originalFactory(sp);

var throwOnTransientDisposable =
sp.GetRequiredService<ThrowOnTransientDisposable>();
if (throwOnTransientDisposable.ShouldThrow &&
originalResult is IDisposable d)
{
throw new InvalidOperationException("Trying to
resolve " +
$"transient disposable service
{d.GetType().Name} in " +
"the wrong scope. Use an
'OwningComponentBase<T>' " +
"component base class for the service 'T' you
are " +
"trying to resolve.");
}

return originalResult;
},
original.Lifetime);

return newDescriptor;
}

private ServiceDescriptor CreatePatchedDescriptor(


ServiceDescriptor original)
{
var newDescriptor = new ServiceDescriptor(
original.ServiceType,
(sp) => {
var throwOnTransientDisposable =
sp.GetRequiredService<ThrowOnTransientDisposable>();
if (throwOnTransientDisposable.ShouldThrow)
{
throw new InvalidOperationException("Trying to
resolve " +
"transient disposable service " +
$"{original.ImplementationType?.Name} in the
wrong " +
"scope. Use an 'OwningComponentBase<T>'
component " +
"base class for the service 'T' you are trying
to " +
"resolve.");
}

if (original.ImplementationType is null)
{
throw new InvalidOperationException(
"ImplementationType is null.");
}

return ActivatorUtilities.CreateInstance(sp,
original.ImplementationType);
},
ServiceLifetime.Transient);

return newDescriptor;
}
}

internal class ThrowOnTransientDisposable


{
public bool ShouldThrow { get; set; }
}
}

TransitiveTransientDisposableDependency.cs :

C#

public class TransitiveTransientDisposableDependency


: ITransitiveTransientDisposableDependency, IDisposable
{
public void Dispose() { }
}

public interface ITransitiveTransientDisposableDependency


{
}

public class TransientDependency


{
private readonly ITransitiveTransientDisposableDependency
transitiveTransientDisposableDependency;

public TransientDependency(ITransitiveTransientDisposableDependency
transitiveTransientDisposableDependency)
{
this.transitiveTransientDisposableDependency =
transitiveTransientDisposableDependency;
}
}

The TransientDependency in the following example is detected.

In Program.cs :

C#

builder.DetectIncorrectUsageOfTransients();
builder.Services.AddTransient<TransientDependency>();
builder.Services.AddTransient<ITransitiveTransientDisposableDependency,
TransitiveTransientDisposableDependency>();

The app can register transient disposables without throwing an exception. However,
attempting to resolve a transient disposable results in an InvalidOperationException, as
the following example shows.

Pages/TransientExample.razor :

razor

@page "/transient-example"
@inject TransientDependency TransientDependency

<h1>Transient Disposable Detection</h1>

Navigate to the TransientExample component at /transient-example and an


InvalidOperationException is thrown when the framework attempts to construct an
instance of TransientDependency :

System.InvalidOperationException: Trying to resolve transient disposable service


TransientDependency in the wrong scope. Use an 'OwningComponentBase<T>'
component base class for the service 'T' you are trying to resolve.

Access Blazor services from a different DI scope


This section only applies to Blazor Server apps.

There may be times when a Razor component invokes asynchronous methods that
execute code in a different DI scope. Without the correct approach, these DI scopes
don't have access to Blazor's services, such as IJSRuntime and
Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.
For example, HttpClient instances created using IHttpClientFactory have their own DI
service scope. As a result, HttpMessageHandler instances configured on the HttpClient
aren't able to directly inject Blazor services.

Create a class BlazorServiceAccessor that defines an AsyncLocal, which stores the


Blazor IServiceProvider for the current asynchronous context. A BlazorServiceAcccessor
instance can be acquired from within a different DI service scope to access Blazor
services.

BlazorServiceAccessor.cs :

C#

internal sealed class BlazorServiceAccessor


{
private static readonly AsyncLocal<BlazorServiceHolder>
s_currentServiceHolder = new();

public IServiceProvider? Services


{
get => s_currentServiceHolder.Value?.Services;
set
{
if (s_currentServiceHolder.Value is { } holder)
{
// Clear the current IServiceProvider trapped in the
AsyncLocal.
holder.Services = null;
}

if (value is not null)


{
// Use object indirection to hold the IServiceProvider in an
AsyncLocal
// so it can be cleared in all ExecutionContexts when it's
cleared.
s_currentServiceHolder.Value = new() { Services = value };
}
}
}

private sealed class BlazorServiceHolder


{
public IServiceProvider? Services { get; set; }
}
}

To set the value of BlazorServiceAccessor.Services automatically when an async


component method is invoked, create a custom base component that re-implements
the three primary asynchronous entry points into Razor component code:
IComponent.SetParametersAsync
IHandleEvent.HandleEventAsync
IHandleAfterRender.OnAfterRenderAsync

The following class demonstrates the implementation for the base component.

CustomComponentBase.cs :

C#

using Microsoft.AspNetCore.Components;

public class CustomComponentBase : ComponentBase, IHandleEvent,


IHandleAfterRender
{
private bool hasCalledOnAfterRender;

[Inject]
private IServiceProvider Services { get; set; } = default!;

[Inject]
private BlazorServiceAccessor BlazorServiceAccessor { get; set; } =
default!;

public override Task SetParametersAsync(ParameterView parameters)


=> InvokeWithBlazorServiceContext(() =>
base.SetParametersAsync(parameters));

Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback,


object? arg)
=> InvokeWithBlazorServiceContext(() =>
{
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion
&&
task.Status != TaskStatus.Canceled;

StateHasChanged();

return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
});

Task IHandleAfterRender.OnAfterRenderAsync()
=> InvokeWithBlazorServiceContext(() =>
{
var firstRender = !hasCalledOnAfterRender;
hasCalledOnAfterRender |= true;

OnAfterRender(firstRender);

return OnAfterRenderAsync(firstRender);
});

private async Task CallStateHasChangedOnAsyncCompletion(Task task)


{
try
{
await task;
}
catch
{
if (task.IsCanceled)
{
return;
}

throw;
}

StateHasChanged();
}

private async Task InvokeWithBlazorServiceContext(Func<Task> func)


{
try
{
BlazorServiceAccessor.Services = Services;
await func();
}
finally
{
BlazorServiceAccessor.Services = null;
}
}
}

Any components extending CustomComponentBase automatically have


BlazorServiceAccessor.Services set to the IServiceProvider in the current Blazor DI

scope.

Finally, in Program.cs , add the BlazorServiceAccessor as a scoped service:

C#

builder.Services.AddScoped<BlazorServiceAccessor>();

Additional resources
Dependency injection in ASP.NET Core
IDisposable guidance for Transient and shared instances
Dependency injection into views in ASP.NET Core
ASP.NET Core Blazor startup
Article • 04/13/2023

This article explains how to configure Blazor startup.

The Blazor startup process is automatic and asynchronous via the Blazor script ( blazor.
{server|webassembly}.js ), where the {server|webassembly} placeholder is either server
for Blazor Server or webassembly for Blazor WebAssembly. The Blazor <script> tag is
found in the wwwroot/index.html file of a Blazor WebAssembly app or the
Pages/_Host.cshtml file of a Blazor Server app.

To manually start Blazor:

Add an autostart="false" attribute and value to the Blazor <script> tag.


Place a script that calls Blazor.start() after the Blazor <script> tag and inside
the closing </body> tag.

JavaScript initializers
JavaScript (JS) initializers execute logic before and after a Blazor app loads. JS initializers
are useful in the following scenarios:

Customizing how a Blazor app loads.


Initializing libraries before Blazor starts up.
Configuring Blazor settings.

JS initializers are detected as part of the build process and imported automatically in
Blazor apps. Use of JS initializers often removes the need to manually trigger script
functions from the app when using Razor class libraries (RCLs).

To define a JS initializer, add a JS module to the project named {NAME}.lib.module.js ,


where the {NAME} placeholder is the assembly name, library name, or package identifier.
Place the file in the project's web root, which is typically the wwwroot folder.

The module exports either or both of the following conventional functions:

beforeStart(options, extensions) : Called before Blazor starts. For example,

beforeStart is used to customize the loading process, logging level, and other
options specific to the hosting model.
In Blazor WebAssembly, beforeStart receives the Blazor WebAssembly options
( options in this section's examples) and any extensions ( extensions in this
section's examples) added during publishing. For example, options can specify
the use of a custom boot resource loader.
In Blazor Server, beforeStart receives SignalR circuit start options ( options in
this section's examples).
In BlazorWebViews, no options are passed.
afterStarted : Called after Blazor is ready to receive calls from JS. For example,

afterStarted is used to initialize libraries by making JS interop calls and


registering custom elements. The Blazor instance is passed to afterStarted as an
argument ( blazor in this section's example).

For the file name:

If the JS initializers are consumed as a static asset in the project, use the format
{ASSEMBLY NAME}.lib.module.js , where the {ASSEMBLY NAME} placeholder is the
app's assembly name. For example, name the file BlazorSample.lib.module.js for a
project with an assembly name of BlazorSample . Place the file in the app's wwwroot
folder.
If the JS initializers are consumed from an RCL, use the format {LIBRARY
NAME/PACKAGE ID}.lib.module.js , where the {LIBRARY NAME/PACKAGE ID}
placeholder is the project's library name or package identifier. For example, name
the file RazorClassLibrary1.lib.module.js for an RCL with a package identifier of
RazorClassLibrary1 . Place the file in the library's wwwroot folder.

The following example demonstrates JS initializers that load custom scripts before and
after Blazor has started by appending them to the <head> in beforeStart and
afterStarted :

JavaScript

export function beforeStart(options, extensions) {


var customScript = document.createElement('script');
customScript.setAttribute('src', 'beforeStartScripts.js');
document.head.appendChild(customScript);
}

export function afterStarted(blazor) {


var customScript = document.createElement('script');
customScript.setAttribute('src', 'afterStartedScripts.js');
document.head.appendChild(customScript);
}

The preceding beforeStart example only guarantees that the custom script loads
before Blazor starts. It doesn't guarantee that awaited promises in the script complete
their execution before Blazor starts.

7 Note

MVC and Razor Pages apps don't automatically load JS initializers. However,
developer code can include a script to fetch the app's manifest and trigger the load
of the JS initializers.

For an examples of JS initializers, see the following resources:

Use Razor components in JavaScript apps and SPA frameworks ( quoteContainer2


example)
ASP.NET Core Blazor event handling (Custom clipboard paste event example)
Deployment layout for ASP.NET Core Blazor WebAssembly apps
Basic Test App in the ASP.NET Core GitHub repository
(BasicTestApp.lib.module.js)

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Ensure libraries are loaded in a specific order


Append custom scripts to the <head> in beforeStart and afterStarted in the order that
they should load.

The following example loads script1.js before script2.js and script3.js before
script4.js :

JavaScript

export function beforeStart(options, extensions) {


var customScript1 = document.createElement('script');
customScript1.setAttribute('src', 'script1.js');
document.head.appendChild(customScript1);

var customScript2 = document.createElement('script');


customScript2.setAttribute('src', 'script2.js');
document.head.appendChild(customScript2);
}

export function afterStarted(blazor) {


var customScript1 = document.createElement('script');
customScript1.setAttribute('src', 'script3.js');
document.head.appendChild(customScript1);

var customScript2 = document.createElement('script');


customScript2.setAttribute('src', 'script4.js');
document.head.appendChild(customScript2);
}

Import additional modules


Use top-level import statements in the JS initializers file ( *.lib.module.js ) to import
additional modules.

additionalModule.js :

JavaScript

export function logMessage() {


console.log('logMessage is logging');
}

JavaScript

import { logMessage } from "/additionalModule.js";

export function beforeStart(options, extensions) {


...

logMessage();
}

Import map
Import maps are supported by ASP.NET Core and Blazor.

Initialize Blazor when the document is ready


The following example starts Blazor when the document is ready:

CSHTML
<body>
...

<script src="_framework/blazor.{server|webassembly}.js"
autostart="false"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
Blazor.start();
});
</script>
</body>

The {server|webassembly} placeholder in the preceding markup is either server for a


Blazor Server app or webassembly for a Blazor WebAssembly app.

Chain to the Promise that results from a


manual start
To perform additional tasks, such as JS interop initialization, use then to chain to the
Promise that results from a manual Blazor app start:

CSHTML

<body>
...

<script src="_framework/blazor.{server|webassembly}.js"
autostart="false"></script>
<script>
Blazor.start().then(function () {
...
});
</script>
</body>

The {server|webassembly} placeholder in the preceding markup is either server for a


Blazor Server app or webassembly for a Blazor WebAssembly app.

7 Note

For a library to automatically execute additional tasks after Blazor has started, use a
JavaScript initializer. Use of a JS initializer doesn't require the consumer of the
library to chain JS calls to Blazor's manual start.
Load boot resources
This section only applies to Blazor WebAssembly apps.

When a Blazor WebAssembly app loads in the browser, the app downloads boot
resources from the server:

JavaScript code to bootstrap the app


.NET runtime and assemblies
Locale specific data

Customize how these boot resources are loaded using the loadBootResource API. The
loadBootResource function overrides the built-in boot resource loading mechanism. Use
loadBootResource for the following scenarios:

Load static resources, such as timezone data or dotnet.wasm , from a CDN.


Load compressed assemblies using an HTTP request and decompress them on the
client for hosts that don't support fetching compressed contents from the server.
Alias resources to a different name by redirecting each fetch request to a new
name.

7 Note

External sources must return the required Cross-Origin Resource Sharing (CORS)
headers for browsers to allow cross-origin resource loading. CDNs usually provide
the required headers by default.

loadBootResource parameters appear in the following table.

Parameter Description

type The type of the resource. Permissible types include: assembly , pdb , dotnetjs ,
dotnetwasm , and timezonedata . You only need to specify types for custom behaviors.
Types not specified to loadBootResource are loaded by the framework per their
default loading behaviors. The dotnetjs boot resource ( dotnet.*.js ) must either
return null for default loading behavior or a URI for the source of the dotnetjs
boot resource.

name The name of the resource.

defaultUri The relative or absolute URI of the resource.

integrity The integrity string representing the expected content in the response.
The loadBootResource function can return a URI string to override the loading process.
In the following example, the following files from bin/Release/{TARGET
FRAMEWORK}/wwwroot/_framework are served from a CDN at

https://cdn.example.com/blazorwebassembly/{VERSION}/ :

dotnet.*.js

dotnet.wasm

Timezone data

The {TARGET FRAMEWORK} placeholder is the target framework moniker (for example,
net7.0 ). The {VERSION} placeholder is the shared framework version (for example,
7.0.0 ).

Inside the closing </body> tag of wwwroot/index.html :

HTML

<script src="_framework/blazor.webassembly.js" autostart="false"></script>


<script>
Blazor.start({
loadBootResource: function (type, name, defaultUri, integrity) {
console.log(`Loading: '${type}', '${name}', '${defaultUri}',
'${integrity}'`);
switch (type) {
case 'dotnetjs':
case 'dotnetwasm':
case 'timezonedata':
return
`https://cdn.example.com/blazorwebassembly/{VERSION}/${name}`;
}
}
});
</script>

To customize more than just the URLs for boot resources, the loadBootResource function
can call fetch directly and return the result. The following example adds a custom HTTP
header to the outbound requests. To retain the default integrity checking behavior, pass
through the integrity parameter.

Inside the closing </body> tag of wwwroot/index.html :

HTML

<script src="_framework/blazor.webassembly.js" autostart="false"></script>


<script>
Blazor.start({
loadBootResource: function (type, name, defaultUri, integrity) {
if (type == 'dotnetjs') {
return null;
} else {
return fetch(defaultUri, {
cache: 'no-cache',
integrity: integrity,
headers: { 'Custom-Header': 'Custom Value' }
});
}
}
});
</script>

When the loadBootResource function returns null , Blazor uses the default loading
behavior for the resource. For example, the preceding code returns null for the
dotnetjs boot resource ( dotnet.*.js ) because the dotnetjs boot resource must either

return null for default loading behavior or a URI for the source of the dotnetjs boot
resource.

The loadBootResource function can also return a Response promise . For an example,
see Host and deploy ASP.NET Core Blazor WebAssembly.

Control headers in C# code


Control headers at startup in C# code using the following approaches.

In the following examples, a Content Security Policy (CSP) is applied to the app via a
CSP header. The {POLICY STRING} placeholder is the CSP policy string.

In Blazor Server and prerendered Blazor WebAssembly apps, use ASP.NET Core
Middleware to control the headers collection.

In Program.cs :

C#

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


{
context.Response.Headers.Add("Content-Security-Policy", "{POLICY
STRING}");
await next();
});

The preceding example uses inline middleware, but you can also create a custom
middleware class and call the middleware with an extension method in Program.cs . For
more information, see Write custom ASP.NET Core middleware.

In hosted Blazor WebAssembly apps that aren't prerendered, pass StaticFileOptions


to MapFallbackToFile that specifies response headers at the OnPrepareResponse
stage.

In Program.cs of the Server project:

C#

var staticFileOptions = new StaticFileOptions


{
OnPrepareResponse = context =>
{
context.Context.Response.Headers.Add("Content-Security-Policy",
"{POLICY STRING}");
}
};

...

app.MapFallbackToFile("index.html", staticFileOptions);

For more information on CSPs, see Enforce a Content Security Policy for ASP.NET Core
Blazor.

Loading progress indicators


This section only applies to Blazor WebAssembly apps.

The Blazor WebAssembly project template contains Scalable Vector Graphics (SVG) and
text indicators that show the loading progress of the app.

The progress indicators are implemented with HTML and CSS using two CSS custom
properties (variables) provided by Blazor WebAssembly:

--blazor-load-percentage : The percentage of app files loaded.

--blazor-load-percentage-text : The percentage of app files loaded, rounded to

the nearest whole number.

Using the preceding CSS variables, you can create custom progress indicators that
match the styling of your app.

In the following example:


resourcesLoaded is an instantaneous count of the resources loaded during app

startup.
totalResources is the total number of resources to load.

JavaScript

const percentage = resourcesLoaded / totalResources * 100;


document.documentElement.style.setProperty(
'--blazor-load-percentage', `${percentage}%`);
document.documentElement.style.setProperty(
'--blazor-load-percentage-text', `"${Math.floor(percentage)}%"`);

The default round progress indicator is implemented in HTML in the wwwroot/index.html


file:

HTML

<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>

To review the Blazor WebAssembly project template markup and styling for the default
progress indicators, see the ASP.NET Core reference source:

wwwroot/index.html
app.css

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Instead of using the default round progress indicator, the following example shows how
to implement a linear progress indicator.

Add the following styles to wwwroot/css/app.css :


css

.linear-progress {
background: silver;
width: 50vw;
margin: 20% auto;
height: 1rem;
border-radius: 10rem;
overflow: hidden;
position: relative;
}

.linear-progress:after {
content: '';
position: absolute;
inset: 0;
background: blue;
scale: var(--blazor-load-percentage, 0%) 100%;
transform-origin: left top;
transition: scale ease-out 0.5s;
}

A CSS variable ( var(...) ) is used to pass the value of --blazor-load-percentage to the


scale property of a blue pseudo-element that indicates the loading progress of the
app's files. As the app loads, --blazor-load-percentage is updated automatically, which
dynamically changes the progress indicator's visual representation.

In wwwroot/index.html , remove the default SVG round indicator in <div id="app">...


</div> and replace it with the following markup:

HTML

<div class="linear-progress"></div>

Additional resources
Environments: Set the app's environment
SignalR (includes sections on SignalR startup configuration)
Globalization and localization: Statically set the culture with Blazor.start() (Blazor
WebAssembly only)
Host and deploy: Blazor WebAssembly: Compression
ASP.NET Core Blazor environments
Article • 04/04/2023

This article explains ASP.NET Core environments in Blazor, including how to set the
environment.

) Important

This topic applies to Blazor WebAssembly. For general guidance on ASP.NET Core
app configuration, which describes the approaches to use for Blazor Server apps,
see Use multiple environments in ASP.NET Core.

For Blazor Server app configuration for static files in environments other than the
Development environment during development and testing (for example, Staging),
see ASP.NET Core Blazor static files.

When running an app locally, the environment defaults to Development . When the app is
published, the environment defaults to Production .

The environment is set using any of the following approaches:

Blazor start configuration


Blazor-Environment header
Azure App Service

The client-side Blazor app (Client) of a hosted Blazor WebAssembly solution determines
the environment from the Server app of the solution via a middleware that
communicates the environment to the browser. The Server app adds a header named
Blazor-Environment with the environment as the value of the header. The Client app

reads the header and sets the environment when the WebAssemblyHost is created in
Program.cs (WebAssemblyHostBuilder.CreateDefault). The Server app of the solution is
an ASP.NET Core app, so more information on how to configure the environment is
found in Use multiple environments in ASP.NET Core.

For a standalone Blazor WebAssembly app running locally, the development server adds
the Blazor-Environment header to specify the Development environment.

Set the environment via startup configuration


The following example starts Blazor in the Staging environment if the hostname
includes localhost . Otherwise, the environment is set to Production .

Inside the closing </body> tag of wwwroot/index.html :

CSHTML

<script src="_framework/blazor.webassembly.js" autostart="false"></script>


<script>
if (window.location.hostname.includes("localhost")) {
Blazor.start({
environment: "Staging"
});
} else {
Blazor.start({
environment: "Production"
});
}
</script>

Using the environment property overrides the environment set by the Blazor-
Environment header.

For more information on Blazor startup, see ASP.NET Core Blazor startup.

Set the environment via header


To specify the environment for other hosting environments, add the Blazor-Environment
header.

In the following example for IIS, the custom header ( Blazor-Environment ) is added to
the published web.config file. The web.config file is located in the bin/Release/{TARGET
FRAMEWORK}/publish folder, where the placeholder {TARGET FRAMEWORK} is the target

framework:

XML

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


<configuration>
<system.webServer>

...

<httpProtocol>
<customHeaders>
<add name="Blazor-Environment" value="Staging" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>

7 Note

To use a custom web.config file for IIS that isn't overwritten when the app is
published to the publish folder, see Host and deploy ASP.NET Core Blazor
WebAssembly.

Set the environment for Azure App Service


The guidance in this section requires the use of a hosted Blazor WebAssembly app.

7 Note

For standalone Blazor Webassembly apps, set the environment manually via start
configuration or the Blazor-Environment header.

Use the following guidance for hosted Blazor WebAssembly solutions hosted by Azure
App Service:

1. Confirm that the casing of environment segments in app settings file names
matches their environment name casing exactly. For example, the matching app
settings file name for the Staging environment is appsettings.Staging.json . If the
file name is appsettings.staging.json (lowercase " s "), the file isn't located, and
the settings in the file aren't used in the Staging environment.

2. In the Azure portal for the environment's deployment slot, set the environment
with the ASPNETCORE_ENVIRONMENT app setting. For an app named
BlazorAzureAppSample , the staging App Service Slot is named
BlazorAzureAppSample/Staging . For the Staging slot's configuration, create an app

setting for ASPNETCORE_ENVIRONMENT with a value of Staging . Deployment slot


setting is enabled for the setting.

3. For Visual Studio deployment, confirm that the app is deployed to the correct
deployment slot. For an app named BlazorAzureAppSample , the app is deployed to
the Staging deployment slot.
When requested in a browser, the BlazorAzureAppSample/Staging app loads in the
Staging environment at https://blazorazureappsample-staging.azurewebsites.net .

When the app is loaded in the browser, the response header collection for
blazor.boot.json indicates that the Blazor-Environment header value is Staging .

App settings from the appsettings.{ENVIRONMENT}.json file are loaded by the app, where
the {ENVIRONMENT} placeholder is the app's environment. In the preceding example,
settings from the appsettings.Staging.json file are loaded.

Read the environment


Obtain the app's environment in a component by injecting
IWebAssemblyHostEnvironment and reading the Environment property.

Pages/ReadEnvironment.razor :

razor

@page "/read-environment"
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@inject IWebAssemblyHostEnvironment HostEnvironment

<h1>Environment example</h1>

<p>Environment: @HostEnvironment.Environment</p>

During startup, the WebAssemblyHostBuilder exposes the


IWebAssemblyHostEnvironment through the HostEnvironment property, which enables
environment-specific logic in host builder code.

In Program.cs :

C#

if (builder.HostEnvironment.Environment == "Custom")
{
...
};

The following convenience extension methods provided through


WebAssemblyHostEnvironmentExtensions permit checking the current environment for
Development , Production , Staging , and custom environment names:
IsDevelopment
IsProduction
IsStaging
IsEnvironment

In Program.cs :

C#

if (builder.HostEnvironment.IsStaging())
{
...
};

if (builder.HostEnvironment.IsEnvironment("Custom"))
{
...
};

The IWebAssemblyHostEnvironment.BaseAddress property can be used during startup


when the NavigationManager service isn't available.

Additional resources
ASP.NET Core Blazor startup
Use multiple environments in ASP.NET Core
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor logging
Article • 04/04/2023

This article explains logging in Blazor apps, including configuration and how to write log
messages from Razor components.

Configuration
Logging configuration can be loaded from app settings files. For more information, see
ASP.NET Core Blazor configuration.

At default log levels and without configuring additional logging providers:

Blazor Server apps only log to the server-side .NET console in the Development
environment at the LogLevel.Information level or higher.
Blazor WebAssembly apps only log to the client-side browser developer tools
console at the LogLevel.Information level or higher.

When the app is configured in the project file to use implicit namespaces
( <ImplicitUsings>enable</ImplicitUsings> ), a using directive for
Microsoft.Extensions.Logging or any API in the LoggerExtensions class isn't required to
support API Visual Studio IntelliSense completions or building apps. If implicit
namespaces aren't enabled, Razor components must explicitly define @using directives
for logging namespaces that aren't imported via the _Imports.razor file.

Log levels
Log levels in Blazor apps conform to ASP.NET Core app log levels, which are listed in the
API documentation at LogLevel.

Razor component logging


The following example:

Injects an ILogger ( ILogger<Counter> ) object to create a logger. The log's category


is the fully qualified name of the component's type, Counter .
Calls LogWarning to log at the Warning level.

Pages/Counter1.razor :
razor

@page "/counter-1"
@inject ILogger<Counter> logger

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
logger.LogWarning("Someone has clicked me!");

currentCount++;
}
}

The following example demonstrates logging with an ILoggerFactory in components.

Pages/Counter2.razor :

razor

@page "/counter-2"
@inject ILoggerFactory LoggerFactory

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
var logger = LoggerFactory.CreateLogger<Counter>();
logger.LogWarning("Someone has clicked me!");

currentCount++;
}
}

Logging in Blazor Server apps


For general ASP.NET Core logging guidance that pertains to Blazor Server, see Logging
in .NET Core and ASP.NET Core.

Logging in Blazor WebAssembly apps


Not every feature of ASP.NET Core logging is supported in Blazor WebAssembly apps.
For example, Blazor WebAssembly apps don't have access to the client's file system or
network, so writing logs to the client's physical or network storage isn't possible. When
using a third-party logging service designed to work with single-page apps (SPAs),
follow the service's security guidance. Keep in mind that every piece of data, including
keys or secrets stored in the Blazor WebAssembly app are insecure and can be easily
discovered by malicious users.

Configure logging in Blazor WebAssembly apps with the


WebAssemblyHostBuilder.Logging property. The Logging property is of type
ILoggingBuilder, so the extension methods of ILoggingBuilder are supported.

To set the minimum logging level, call LoggingBuilderExtensions.SetMinimumLevel on


the host builder in Program.cs with the LogLevel. The following example sets the
minimum log level to Warning:

C#

builder.Logging.SetMinimumLevel(LogLevel.Warning);

Log in Program.cs (Blazor WebAssembly)


Logging in Program.cs is supported in Blazor WebAssembly apps after the
WebAssemblyHostBuilder is built using the framework's internal console logger provider
(WebAssemblyConsoleLoggerProvider (reference source) ).

In Program.cs :

C#

var host = builder.Build();

var logger = host.Services.GetRequiredService<ILoggerFactory>()


.CreateLogger<Program>();

logger.LogInformation("Logged after the app is built in Program.cs.");

await host.RunAsync();
Developer tools console output:

info: Program[0]
Logged after the app is built in Program.cs.

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Log category (Blazor WebAssembly)


Log categories are supported in Blazor WebAssembly apps.

The following example shows how to use log categories with the Counter component of
an app created from a Blazor project template.

In the IncrementCount method of the app's Counter component ( Pages/Counter.razor )


that injects an ILoggerFactory as LoggerFactory :

C#

var logger = LoggerFactory.CreateLogger("CustomCategory");


logger.LogWarning("Someone has clicked me!");

Developer tools console output:

warn: CustomCategory[0]
Someone has clicked me!

Log event ID (Blazor WebAssembly)


Log event ID is supported in Blazor WebAssembly apps.

The following example shows how to use log event IDs with the Counter component of
an app created from a Blazor project template.

LogEvent.cs :
C#

public class LogEvent


{
public const int Event1 = 1000;
public const int Event2 = 1001;
}

In the IncrementCount method of the app's Counter component ( Pages/Counter.razor ):

C#

logger.LogInformation(LogEvent.Event1, "Someone has clicked me!");


logger.LogWarning(LogEvent.Event2, "Someone has clicked me!");

Developer tools console output:

info: BlazorSample.Pages.Counter[1000]
Someone has clicked me!
warn: BlazorSample.Pages.Counter[1001]
Someone has clicked me!

Log message template (Blazor WebAssembly)


Log message templates are supported in Blazor WebAssembly apps:

The following example shows how to use log message templates with the Counter
component of an app created from a Blazor project template.

In the IncrementCount method of the app's Counter component ( Pages/Counter.razor ):

C#

logger.LogInformation("Someone clicked me at {CurrentDT}!",


DateTime.UtcNow);

Developer tools console output:

info: BlazorSample.Pages.Counter[0]
Someone clicked me at 04/21/2022 12:15:57!
Log exception parameters (Blazor
WebAssembly)
Log exception parameters are supported in Blazor WebAssembly apps.

The following example shows how to use log exception parameters with the Counter
component of an app created from a Blazor project template.

In the IncrementCount method of the app's Counter component ( Pages/Counter.razor ):

C#

currentCount++;

try
{
if (currentCount == 3)
{
currentCount = 4;
throw new OperationCanceledException("Skip 3");
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Exception (currentCount: {Count})!",
currentCount);
}

Developer tools console output:

warn: BlazorSample.Pages.Counter[0]
Exception (currentCount: 4)!
System.OperationCanceledException: Skip 3
at BlazorSample.Pages.Counter.IncrementCount() in
C:UsersAlabaDesktopBlazorSamplePagesCounter.razor:line 28

Filter function (Blazor WebAssembly)


Filter functions are supported in Blazor WebAssembly apps.

The following example shows how to use a filter with the Counter component of an app
created from a Blazor project template.

In Program.cs :
C#

builder.Logging.AddFilter((provider, category, logLevel) =>


category.Equals("CustomCategory2") && logLevel == LogLevel.Information);

In the IncrementCount method of the app's Counter component ( Pages/Counter.razor )


that injects an ILoggerFactory as LoggerFactory :

C#

var logger1 = LoggerFactory.CreateLogger("CustomCategory1");


logger1.LogInformation("Someone has clicked me!");

var logger2 = LoggerFactory.CreateLogger("CustomCategory1");


logger2.LogWarning("Someone has clicked me!");

var logger3 = LoggerFactory.CreateLogger("CustomCategory2");


logger3.LogInformation("Someone has clicked me!");

var logger4 = LoggerFactory.CreateLogger("CustomCategory2");


logger4.LogWarning("Someone has clicked me!");

In the developer tools console output, the filter only permits logging for the
CustomCategory2 category and Information log level message:

info: CustomCategory2[0]
Someone has clicked me!

The app can also configure log filtering for specific namespaces. For example, set the
log level to Trace in Program.cs :

C#

builder.Logging.SetMinimumLevel(LogLevel.Trace);

Normally at the Trace log level, developer tools console output at the Verbose level
includes Microsoft.AspNetCore.Components.RenderTree logging messages, such as the
following:

dbug: Microsoft.AspNetCore.Components.RenderTree.Renderer[3]
Rendering component 14 of type
Microsoft.AspNetCore.Components.Web.HeadOutlet
In Program.cs , logging messages specific to
Microsoft.AspNetCore.Components.RenderTree can be disabled using either of the
following approaches:

C#

builder.Logging.AddFilter("Microsoft.AspNetCore.Components.RenderTree.*
", LogLevel.None);

C#

builder.Services.PostConfigure<LoggerFilterOptions>(options =>
options.Rules.Add(
new LoggerFilterRule(null,

"Microsoft.AspNetCore.Components.RenderTree.*",
LogLevel.None,
null)
));

After either of the preceding filters is added to the app, the console output at the
Verbose level doesn't show logging messages from the
Microsoft.AspNetCore.Components.RenderTree API.

Custom logger provider (Blazor WebAssembly)


The example in this section demonstrates a custom logger provider for further
customization.

Add a package reference to the app for the


Microsoft.Extensions.Logging.Configuration package.

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 .

Add the following custom logger configuration. The configuration establishes a


LogLevels dictionary that sets a custom log format for three log levels: Information,

Warning, and Error. A LogFormat enum is used to describe short ( LogFormat.Short ) and
long ( LogFormat.Long ) formats.
CustomLoggerConfiguration.cs :

C#

using Microsoft.Extensions.Logging;

public class CustomLoggerConfiguration


{
public int EventId { get; set; }

public Dictionary<LogLevel, LogFormat> LogLevels { get; set; } =


new()
{
[LogLevel.Information] = LogFormat.Short,
[LogLevel.Warning] = LogFormat.Short,
[LogLevel.Error] = LogFormat.Long
};

public enum LogFormat


{
Short,
Long
}
}

Add the following custom logger to the app. The CustomLogger outputs custom log
formats based on the logLevel values defined in the preceding
CustomLoggerConfiguration configuration.

C#

using Microsoft.Extensions.Logging;
using static CustomLoggerConfiguration;

public sealed class CustomLogger : ILogger


{
private readonly string name;
private readonly Func<CustomLoggerConfiguration> getCurrentConfig;

public CustomLogger(
string name,
Func<CustomLoggerConfiguration> getCurrentConfig) =>
(this.name, this.getCurrentConfig) = (name, getCurrentConfig);

public IDisposable BeginScope<TState>(TState state) => default!;

public bool IsEnabled(LogLevel logLevel) =>


getCurrentConfig().LogLevels.ContainsKey(logLevel);

public void Log<TState>(


LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}

CustomLoggerConfiguration config = getCurrentConfig();

if (config.EventId == 0 || config.EventId == eventId.Id)


{
switch (config.LogLevels[logLevel])
{
case LogFormat.Short:
Console.WriteLine($"{name}: {formatter(state,
exception)}");
break;
case LogFormat.Long:
Console.WriteLine($"[{eventId.Id, 2}: {logLevel, -12}]
{name} - {formatter(state, exception)}");
break;
default:
// No-op
break;
}
}
}
}

Add the following custom logger provider to the app. CustomLoggerProvider adopts an
Options-based approach to configure the logger via built-in logging configuration
features. For example, the app can set or change log formats via an appsettings.json
file without requiring code changes to the custom logger, which is demonstrated at the
end of this section.

CustomLoggerProvider.cs :

C#

using System.Collections.Concurrent;
using Microsoft.Extensions.Options;

[ProviderAlias("CustomLog")]
public sealed class CustomLoggerProvider : ILoggerProvider
{
private readonly IDisposable onChangeToken;
private CustomLoggerConfiguration config;
private readonly ConcurrentDictionary<string, CustomLogger> loggers =
new(StringComparer.OrdinalIgnoreCase);

public CustomLoggerProvider(
IOptionsMonitor<CustomLoggerConfiguration> config)
{
this.config = config.CurrentValue;
onChangeToken = config.OnChange(updatedConfig => this.config =
updatedConfig);
}

public ILogger CreateLogger(string categoryName) =>


loggers.GetOrAdd(categoryName, name => new CustomLogger(name,
GetCurrentConfig));

private CustomLoggerConfiguration GetCurrentConfig() => config;

public void Dispose()


{
loggers.Clear();
onChangeToken.Dispose();
}
}

Add the following custom logger extensions.

CustomLoggerExtensions.cs :

C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration;

public static class CustomLoggerExtensions


{
public static ILoggingBuilder AddCustomLogger(
this ILoggingBuilder builder)
{
builder.AddConfiguration();

builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<ILoggerProvider,
CustomLoggerProvider>());

LoggerProviderOptions.RegisterProviderOptions
<CustomLoggerConfiguration, CustomLoggerProvider>
(builder.Services);

return builder;
}
}
In Program.cs on the host builder, clear the existing provider by calling ClearProviders
and add the custom logging provider:

C#

builder.Logging.ClearProviders().AddCustomLogger();

In the following Index component:

The debug message isn't logged.


The information message is logged in the short format ( LogFormat.Short ).
The warning message is logged in the short format ( LogFormat.Short ).
The error message is logged in the long format ( LogFormat.Long ).
The trace message isn't logged.

Pages/Index.razor :

razor

@page "/"
@using Microsoft.Extensions.Logging
@inject ILogger<Index> Logger

<p>
<button @onclick="LogMessages">Log Messages</button>
</p>

@code{
private void LogMessages()
{
Logger.LogDebug(1, "This is a debug message.");
Logger.LogInformation(3, "This is an information message.");
Logger.LogWarning(5, "This is a warning message.");
Logger.LogError(7, "This is an error message.");
Logger.LogTrace(5!, "This is a trace message.");
}
}

The following output is seen in the browser's developer tools console when the Log
Messages button is selected. The log entries reflect the appropriate formats applied by
the custom logger:

LoggingTest.Pages.Index: This is an information message.


LoggingTest.Pages.Index: This is a warning message.
[ 7: Error ] LoggingTest.Pages.Index - This is an error message.
From a casual inspection of the preceding example, it's apparent that setting the log line
formats via the dictionary in CustomLoggerConfiguration isn't strictly necessary. The line
formats applied by the custom logger ( CustomLogger ) could have been applied by
merely checking the logLevel in the Log method. The purpose of assigning the log
format via configuration is that the developer can change the log format easily via app
configuration, as the following example demonstrates.

In the wwwroot folder, add or update the appsettings.json file to include logging
configuration. Set the log format to Long for all three log levels:

JSON

{
"Logging": {
"CustomLog": {
"LogLevels": {
"Information": "Long",
"Warning": "Long",
"Error": "Long"
}
}
}
}

In the preceding example, notice that the entry for the custom logger configuration is
CustomLog , which was applied to the custom logger provider ( CustomLoggerProvider ) as

an alias with [ProviderAlias("CustomLog")] . The logging configuration could have been


applied with the name CustomLoggerProvider instead of CustomLog , but use of the alias
CustomLog is more user friendly.

In Program.cs consume the logging configuration. Add the following code:

C#

builder.Logging.AddConfiguration(
builder.Configuration.GetSection("Logging"));

The call to LoggingBuilderConfigurationExtensions.AddConfiguration can be placed


either before or after adding the custom logger provider.

Run the app again. Select the Log Messages button. Notice that the logging
configuration is applied from the appsettings.json file. All three log entries are in the
long ( LogFormat.Long ) format:
[ 3: Information ] LoggingTest.Pages.Index - This is an information message.
[ 5: Warning ] LoggingTest.Pages.Index - This is a warning message.
[ 7: Error ] LoggingTest.Pages.Index - This is an error message.

Log scopes (Blazor WebAssembly)


The Blazor WebAssembly developer tools console logger doesn't support log scopes.
However, a custom logger can support log scopes. For an unsupported example that
you can further develop to suit your needs, see the prototype in the dotnet/blazor-
samples GitHub repository:

BlazorWebAssemblyScopesLogger sample app

The sample app uses standard ASP.NET Core BeginScope logging syntax to indicate
scopes for logged messages. The Logger service in the following example is an
ILogger<Index> , which is injected into the app's Index component ( Pages/Index.razor ).

C#

using (Logger.BeginScope("L1"))
{
Logger.LogInformation(3, "INFO: ONE scope.");
}

using (Logger.BeginScope("L1"))
{
using (Logger.BeginScope("L2"))
{
Logger.LogInformation(3, "INFO: TWO scopes.");
}
}

using (Logger.BeginScope("L1"))
{
using (Logger.BeginScope("L2"))
{
using (Logger.BeginScope("L3"))
{
Logger.LogInformation(3, "INFO: THREE scopes.");
}
}
}

Output:

[ 3: Information ] ScopesLogger.Pages.Index - INFO: ONE scope. => L1


blazor.webassembly.js:1:35542
[ 3: Information ] ScopesLogger.Pages.Index - INFO: TWO scopes. => L1 => L2
blazor.webassembly.js:1:35542
[ 3: Information ] ScopesLogger.Pages.Index - INFO: THREE scopes. => L1 => L2 =>
L3

Hosted Blazor WebAssembly logging


A hosted Blazor WebAssembly app that prerenders its content executes component
initialization code twice. Logging takes place server-side on the first execution of
initialization code and client-side on the second execution of initialization code.
Depending on the goal of logging during initialization, check logs server-side, client-
side, or both.

SignalR client logging (Blazor Server)


On the SignalR client builder in Pages/_Host.cshtml , pass in the configureSignalR
configuration object that calls configureLogging with the log level.

For the configureLogging log level value, pass the argument as either the string or
integer log level shown in the following table.

LogLevel String setting Integer setting

Trace trace 0

Debug debug 1

Information information 2

Warning warning 3

Error error 4

Critical critical 5

None none 6

Example 1: Set the Information log level with a string value:

HTML

<script src="_framework/blazor.server.js" autostart="false"></script>


<script>
Blazor.start({
configureSignalR: function (builder) {
builder.configureLogging("information");
}
});
</script>

Example 2: Set the Information log level with an integer value:

HTML

<script src="_framework/blazor.server.js" autostart="false"></script>


<script>
Blazor.start({
configureSignalR: function (builder) {
builder.configureLogging(2);
}
});
</script>

For more information on Blazor startup ( Blazor.start() ), see ASP.NET Core Blazor
startup.

SignalR client logging (Blazor WebAssembly)


In Blazor WebAssembly apps, set up app settings configuration as described in ASP.NET
Core Blazor configuration. Place app settings files in wwwroot that contain a
Logging:LogLevel:HubConnection app setting.

7 Note

As an alternative to using app settings, you can pass the LogLevel as the argument
to LoggingBuilderExtensions.SetMinimumLevel when the hub connection is
created in a Razor component. However, accidentally deploying the app to a
production hosting environment with verbose logging may result in a performance
penalty. We recommend using app settings to set the log level.

Provide a Logging:LogLevel:HubConnection app setting in the default appsettings.json


file and in the Development environment app settings file. Use a typical less-verbose log
level for the default, such as LogLevel.Warning. The default app settings value is what is
used in Staging and Production environments if no app settings files for those
environments are present. Use a verbose log level in the Development environment app
settings file, such as LogLevel.Trace.

wwwroot/appsettings.json :
JSON

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

wwwroot/appsettings.Development.json :

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"HubConnection": "Trace"
}
}
}

) Important

Configuration in the preceding app settings files is only used by the app if the
guidance in ASP.NET Core Blazor configuration is followed.

At the top of the Razor component file ( .razor ):

Inject an ILoggerProvider to add a WebAssemblyConsoleLogger to the logging


providers passed to HubConnectionBuilder. Unlike a traditional ConsoleLogger,
WebAssemblyConsoleLogger is a wrapper around browser-specific logging APIs (for
example, console.log ). Use of WebAssemblyConsoleLogger makes logging possible
within Mono inside a browser context.
Inject an IConfiguration to read the Logging:LogLevel:HubConnection app setting.

7 Note

WebAssemblyConsoleLogger is internal and not supported for direct use in developer


code.
C#

@inject ILoggerProvider LoggerProvider


@inject IConfiguration Config

7 Note

The following example is based on the Index component in the SignalR with
Blazor tutorial. Consult the tutorial for further details.

In the component's OnInitializedAsync method, use


HubConnectionBuilderExtensions.ConfigureLogging to add the logging provider and set
the minimum log level from configuration:

C#

protected override async Task OnInitializedAsync()


{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.ConfigureLogging(builder =>
{
builder.AddProvider(LoggerProvider);
builder.SetMinimumLevel(
Config.GetValue<LogLevel>
("Logging:LogLevel:HubConnection"));
})
.Build();

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


...

await hubConnection.StartAsync();
}

7 Note

In the preceding example, Navigation is an injected NavigationManager.

For more information on setting the app's environment for Blazor WebAssembly, see
ASP.NET Core Blazor environments.

Authentication logging (Blazor WebAssembly)


Log Blazor authentication messages at the LogLevel.Debug or LogLevel.Trace logging
levels with a logging configuration in app settings or by using a log filter for
Microsoft.AspNetCore.Components.WebAssembly.Authentication in Program.cs .

Use either of the following approaches:

In an app settings file (for example, wwwroot/appsettings.Development.json ):

JSON

"Logging": {
"LogLevel": {
"Microsoft.AspNetCore.Components.WebAssembly.Authentication":
"Debug"
}
}

For more information on how to enable a Blazor WebAssembly app to read app
settings files, see ASP.NET Core Blazor configuration.

Using a log filter, the following example:


Activates logging for the Debug build configuration using a C# preprocessor
directive.
Logs Blazor authentication messages at the Debug log level.

C#

#if DEBUG
builder.Logging.AddFilter(
"Microsoft.AspNetCore.Components.WebAssembly.Authentication",
LogLevel.Debug);
#endif

7 Note

Blazor WebAssembly apps only log to the client-side browser developer tools
console.

Additional resources
Logging in .NET Core and ASP.NET Core
Loglevel Enum (API documentation)
Implement a custom logging provider in .NET
Browser developer tools documentation:
Chrome DevTools
Firefox Developer Tools
Microsoft Edge Developer Tools overview
Blazor samples GitHub repository (dotnet/blazor-samples)
Handle errors in ASP.NET Core Blazor
apps
Article • 05/09/2023

This article describes how Blazor manages unhandled exceptions and how to develop
apps that detect and handle errors.

Detailed errors during 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 light yellow bar at the bottom of the screen:

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

The UI for this error handling experience is part of the Blazor project templates.

In a Blazor Server app, customize the experience in the Pages/_Host.cshtml file:

CSHTML

<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until
reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for
details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

In a Blazor WebAssembly app, customize the experience in the wwwroot/index.html file:

HTML

<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

The blazor-error-ui element is normally hidden due to the presence of the display:
none style of the blazor-error-ui CSS class in the site's stylesheet
( wwwroot/css/site.css for Blazor Server or wwwroot/css/app.css for Blazor
WebAssembly). When an error occurs, the framework applies display: block to the
element.

Detailed circuit errors


This section applies to Blazor Server apps.

Client-side errors don't include the call stack and don't provide detail on the cause of
the error, but server logs do contain such information. For development purposes,
sensitive circuit error information can be made available to the client by enabling
detailed errors.

Set CircuitOptions.DetailedErrors to true . For more information and an example, see


ASP.NET Core Blazor SignalR guidance.

An alternative to setting CircuitOptions.DetailedErrors is to set the DetailedErrors


configuration key to true in the app's Development environment settings file
( appsettings.Development.json ). Additionally, set SignalR server-side logging
( Microsoft.AspNetCore.SignalR ) to Debug or Trace for detailed SignalR logging.

appsettings.Development.json :

JSON

{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.SignalR": "Debug"
}
}
}

The DetailedErrors configuration key can also be set to true using the
ASPNETCORE_DETAILEDERRORS environment variable with a value of true on
Development/Staging environment servers or on your local system.

2 Warning

Always avoid exposing error information to clients on the Internet, which is a


security risk.

Manage unhandled exceptions in developer


code
For an app to continue after an error, the app must have error handling logic. Later
sections of this article describe potential sources of unhandled exceptions.

In production, don't render framework exception messages or stack traces in the UI.
Rendering exception messages or stack traces could:

Disclose sensitive information to end users.


Help a malicious user discover weaknesses in an app that can compromise the
security of the app, server, or network.

Blazor Server unhandled exceptions


This section applies to Blazor Server apps.

Blazor Server is a stateful framework. While users interact with an app, they maintain a
connection to the server known as a circuit. The circuit holds active component
instances, plus many other aspects of state, such as:

The most recent rendered output of components.


The current set of event-handling delegates that could be triggered by client-side
events.

If a user opens the app in multiple browser tabs, the user creates multiple independent
circuits.

Blazor treats most unhandled exceptions as fatal to the circuit where they occur. If a
circuit is terminated due to an unhandled exception, the user can only continue to
interact with the app by reloading the page to create a new circuit. Circuits outside of
the one that's terminated, which are circuits for other users or other browser tabs, aren't
affected. This scenario is similar to a desktop app that crashes. The crashed app must be
restarted, but other apps aren't affected.
The framework terminates a circuit when an unhandled exception occurs for the
following reasons:

An unhandled exception often leaves the circuit in an undefined state.


The app's normal operation can't be guaranteed after an unhandled exception.
Security vulnerabilities may appear in the app if the circuit continues in an
undefined state.

Global exception handling


For global exception handling, see the following sections:

Error boundaries
Alternative global exception handling

Error boundaries
Blazor is a single-page application (SPA) client-side framework. The browser serves as
the app's host and thus acts as the processing pipeline for individual Razor components
based on URI requests for navigation and static assets. Unlike ASP.NET Core apps that
run on the server with a middleware processing pipeline, there is no middleware
pipeline that processes requests for Razor components that can be leveraged for global
error handling. However, an app can use an error processing component as a cascading
value to process errors in a centralized way.

Error boundaries provide a convenient approach for handling exceptions. The


ErrorBoundary component:

Renders its child content when an error hasn't occurred.


Renders error UI when an unhandled exception is thrown.

To define an error boundary, use the ErrorBoundary component to wrap existing


content. For example, an error boundary can be added around the body content of the
app's main layout.

Shared/MainLayout.razor :

razor

<main>
<article class="content px-4">
<ErrorBoundary>
@Body
</ErrorBoundary>
</article>
</main>

The app continues to function normally, but the error boundary handles unhandled
exceptions.

Consider the following example, where the Counter component throws an exception if
the count increments past five.

In Pages/Counter.razor :

C#

private void IncrementCount()


{
currentCount++;

if (currentCount > 5)
{
throw new InvalidOperationException("Current count is too big!");
}
}

If the unhandled exception is thrown for a currentCount over five:

The exception is handled by the error boundary.


Error UI is rendered ( An error has occurred. ).

By default, the ErrorBoundary component renders an empty <div> element with the
blazor-error-boundary CSS class for its error content. The colors, text, and icon for the

default UI are defined using CSS in the app's stylesheet in the wwwroot folder, so you're
free to customize the error UI.

You can also change the default error content by setting the ErrorContent property:

razor

<ErrorBoundary>
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<p class="errorUI">Nothing to see here right now. Sorry!</p>
</ErrorContent>
</ErrorBoundary>
Because the error boundary is defined in the layout in the preceding examples, the error
UI is seen regardless of which page the user navigated to. We recommend narrowly
scoping error boundaries in most scenarios. If you do broadly scope an error boundary,
you can reset it to a non-error state on subsequent page navigation events by calling
the error boundary's Recover method:

razor

...

<ErrorBoundary @ref="errorBoundary">
@Body
</ErrorBoundary>

...

@code {
private ErrorBoundary? errorBoundary;

protected override void OnParametersSet()


{
errorBoundary?.Recover();
}
}

Alternative global exception handling


An alternative to using Error boundaries (ErrorBoundary) is to pass a custom error
component as a CascadingValue to child components. An advantage of using a
component over using an injected service or a custom logger implementation is that a
cascaded component can render content and apply CSS styles when an error occurs.

The following Error component example merely logs errors, but methods of the
component can process errors in any way required by the app, including through the
use of multiple error processing methods.

Shared/Error.razor :

razor

@using Microsoft.Extensions.Logging
@inject ILogger<Error> Logger

<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }

public void ProcessError(Exception ex)


{
Logger.LogError("Error:ProcessError - Type: {Type} Message:
{Message}",
ex.GetType(), ex.Message);
}
}

7 Note

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

In the App component, wrap the Router component with the Error component. This
permits the Error component to cascade down to any component of the app where the
Error component is received as a CascadingParameter.

App.razor :

razor

<Error>
<Router ...>
...
</Router>
</Error>

To process errors in a component:

Designate the Error component as a CascadingParameter in the @code block. In


an example Counter component in an app based on a Blazor project template, add
the following Error property:

C#

[CascadingParameter]
public Error? Error { get; set; }

Call an error processing method in any catch block with an appropriate exception
type. The example Error component only offers a single ProcessError method,
but the error processing component can provide any number of error processing
methods to address alternative error processing requirements throughout the app.
In the following Counter component example, an exception is thrown and trapped
when the count is greater than five:

razor

@code {
private int currentCount = 0;

[CascadingParameter]
public Error? Error { get; set; }

private void IncrementCount()


{
try
{
currentCount++;

if (currentCount > 5)
{
throw new InvalidOperationException("Current count is
over five!");
}
}
catch (Exception ex)
{
Error?.ProcessError(ex);
}
}
}

Using the preceding Error component with the preceding changes made to a Counter
component, the browser's developer tools console indicates the trapped, logged error:

Console

fail: BlazorSample.Shared.Error[0]
Error:ProcessError - Type: System.InvalidOperationException Message: Current
count is over five!

If the ProcessError method directly participates in rendering, such as showing a custom


error message bar or changing the CSS styles of the rendered elements, call
StateHasChanged at the end of the ProcessErrors method to rerender the UI.

Because the approaches in this section handle errors with a try-catch statement, a Blazor
Server app's SignalR connection between the client and server isn't broken when an
error occurs and the circuit remains alive. Other unhandled exceptions remain fatal to a
circuit. For more information, see the preceding section on how a Blazor Server app
reacts to unhandled exceptions.
Log errors with a persistent provider
If an unhandled exception occurs, the exception is logged to ILogger instances
configured in the service container. By default, Blazor apps log to console output with
the Console Logging Provider. Consider logging to a location on the server (or backend
web API for Blazor WebAssembly apps) with a provider that manages log size and log
rotation. Alternatively, the app can use an Application Performance Management (APM)
service, such as Azure Application Insights (Azure Monitor).

7 Note

Native Application Insights features to support Blazor WebAssembly apps and


native Blazor framework support for Google Analytics might become available in
future releases of these technologies. For more information, see Support App
Insights in Blazor WASM Client Side (microsoft/ApplicationInsights-dotnet
#2143) and Web analytics and diagnostics (includes links to community
implementations) (dotnet/aspnetcore #5461) . In the meantime, a client-side
Blazor WebAssembly app can use the Application Insights JavaScript SDK with JS
interop to log errors directly to Application Insights from a client-side app.

During development in a Blazor Server app, the app usually sends the full details of
exceptions to the browser's console to aid in debugging. In production, detailed errors
aren't sent to clients, but an exception's full details are logged on the server.

You must decide which incidents to log and the level of severity of logged incidents.
Hostile users might be able to trigger errors deliberately. For example, don't log an
incident from an error where an unknown ProductId is supplied in the URL of a
component that displays product details. Not all errors should be treated as incidents
for logging.

For more information, see the following articles:

ASP.NET Core Blazor logging


Handle errors in ASP.NET Core‡
Create web APIs with ASP.NET Core

‡Applies to Blazor Server apps and other server-side ASP.NET Core apps that are web
API backend apps for Blazor. Blazor WebAssembly apps can trap and send error
information on the client to a web API, which logs the error information to a persistent
logging provider.
Places where errors may occur
Framework and app code may trigger unhandled exceptions in any of the following
locations, which are described further in the following sections of this article:

Component instantiation
Lifecycle methods
Rendering logic
Event handlers
Component disposal
JavaScript interop
Prerendering

Component instantiation
When Blazor creates an instance of a component:

The component's constructor is invoked.


The constructors of DI services supplied to the component's constructor via the
@inject directive or the [Inject] attribute are invoked.

An error in an executed constructor or a setter for any [Inject] property results in an


unhandled exception and stops the framework from instantiating the component. If the
app is a Blazor Server app, the circuit fails. If constructor logic may throw exceptions, the
app should trap the exceptions using a try-catch statement with error handling and
logging.

Lifecycle methods
During the lifetime of a component, Blazor invokes lifecycle methods. If any lifecycle
method throws an exception, synchronously or asynchronously, the exception is fatal to
a Blazor Server circuit. For components to deal with errors in lifecycle methods, add
error handling logic.

In the following example where OnParametersSetAsync calls a method to obtain a


product:

An exception thrown in the ProductRepository.GetProductByIdAsync method is


handled by a try-catch statement.
When the catch block is executed:
loadFailed is set to true , which is used to display an error message to the user.

The error is logged.


razor

@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)


{
<h1>@details.ProductName</h1>
<p>@details.Description</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}

@code {
private ProductDetail? details;
private bool loadFailed;

[Parameter]
public int ProductId { get; set; }

protected override async Task OnParametersSetAsync()


{
try
{
loadFailed = false;
details = await
ProductRepository.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}",
ProductId);
}
}

public class ProductDetail


{
public string? ProductName { get; set; }
public string? Description { get; set; }
}

public interface IProductRepository


{
public Task<ProductDetail> GetProductByIdAsync(int id);
}
}

Rendering logic
The declarative markup in a Razor component file ( .razor ) is compiled into a C#
method called BuildRenderTree. When a component renders, BuildRenderTree executes
and builds up a data structure describing the elements, text, and child components of
the rendered component.

Rendering logic can throw an exception. An example of this scenario occurs when
@someObject.PropertyName is evaluated but @someObject is null . For Blazor Server apps,
an unhandled exception thrown by rendering logic is fatal to the app's circuit.

To prevent a NullReferenceException in rendering logic, check for a null object before


accessing its members. In the following example, person.Address properties aren't
accessed if person.Address is null :

razor

@if (person.Address != null)


{
<div>@person.Address.Line1</div>
<div>@person.Address.Line2</div>
<div>@person.Address.City</div>
<div>@person.Address.Country</div>
}

The preceding code assumes that person isn't null . Often, the structure of the code
guarantees that an object exists at the time the component is rendered. In those cases,
it isn't necessary to check for null in rendering logic. In the prior example, person
might be guaranteed to exist because person is created when the component is
instantiated, as the following example shows:

razor

@code {
private Person person = new();

...
}

Event handlers
Client-side code triggers invocations of C# code when event handlers are created using:

@onclick
@onchange

Other @on... attributes


@bind

Event handler code might throw an unhandled exception in these scenarios.

If the app calls code that could fail for external reasons, trap exceptions using a try-catch
statement with error handling and logging.

If an event handler throws an unhandled exception (for example, a database query fails)
that isn't trapped and handled by developer code:

The framework logs the exception.


In a Blazor Server app, the exception is fatal to the app's circuit.

Component disposal
A component may be removed from the UI, for example, because the user has
navigated to another page. When a component that implements System.IDisposable is
removed from the UI, the framework calls the component's Dispose method.

If the component's Dispose method throws an unhandled exception in a Blazor Server


app, the exception is fatal to the app's circuit.

If disposal logic may throw exceptions, the app should trap the exceptions using a try-
catch statement with error handling and logging.

For more information on component disposal, see ASP.NET Core Razor component
lifecycle.

JavaScript interop
IJSRuntime is registered by the Blazor framework. IJSRuntime.InvokeAsync allows .NET
code to make asynchronous calls to the JavaScript (JS) runtime in the user's browser.

The following conditions apply to error handling with InvokeAsync:

If a call to InvokeAsync fails synchronously, a .NET exception occurs. A call to


InvokeAsync may fail, for example, because the supplied arguments can't be
serialized. Developer code must catch the exception. If app code in an event
handler or component lifecycle method doesn't handle an exception in a Blazor
Server app, the resulting exception is fatal to the app's circuit.
If a call to InvokeAsync fails asynchronously, the .NET Task fails. A call to
InvokeAsync may fail, for example, because the JS-side code throws an exception
or returns a Promise that completed as rejected . Developer code must catch the
exception. If using the await operator, consider wrapping the method call in a try-
catch statement with error handling and logging. Otherwise in a Blazor Server app,
the failing code results in an unhandled exception that's fatal to the app's circuit.
By default, calls to InvokeAsync must complete within a certain period or else the
call times out. The default timeout period is one minute. The timeout protects the
code against a loss in network connectivity or JS code that never sends back a
completion message. If the call times out, the resulting System.Threading.Tasks
fails with an OperationCanceledException. Trap and process the exception with
logging.

Similarly, JS code may initiate calls to .NET methods indicated by the [JSInvokable]
attribute. If these .NET methods throw an unhandled exception:

In a Blazor Server app, the exception is not treated as fatal to the app's circuit.
The JS-side Promise is rejected.

You have the option of using error handling code on either the .NET side or the JS side
of the method call.

For more information, see the following articles:

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


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

Prerendering
Razor components can be prerendered using the Component Tag Helper so that their
rendered HTML markup is returned as part of the user's initial HTTP request.

In Blazor Server, prerendering works by:

Creating a new circuit for all of the prerendered components that are part of the
same page.
Generating the initial HTML.
Treating the circuit as disconnected until the user's browser establishes a SignalR
connection back to the same server. When the connection is established,
interactivity on the circuit is resumed and the components' HTML markup is
updated.
In prerendered Blazor WebAssembly, prerendering works by:

Generating initial HTML on the server for all of the prerendered components that
are part of the same page.
Making the component interactive on the client after the browser has loaded the
app's compiled code and the .NET runtime (if not already loaded) in the
background.

If a component throws an unhandled exception during prerendering, for example,


during a lifecycle method or in rendering logic:

In Blazor Server apps, the exception is fatal to the circuit. In prerendered Blazor
WebAssembly apps, the exception prevents rendering the component.
The exception is thrown up the call stack from the ComponentTagHelper.

Under normal circumstances when prerendering fails, continuing to build and render the
component doesn't make sense because a working component can't be rendered.

To tolerate errors that may occur during prerendering, error handling logic must be
placed inside a component that may throw exceptions. Use try-catch statements with
error handling and logging. Instead of wrapping the ComponentTagHelper in a try-catch
statement, place error handling logic in the component rendered by the
ComponentTagHelper.

Advanced scenarios

Recursive rendering
Components can be nested recursively. This is useful for representing recursive data
structures. For example, a TreeNode component can render more TreeNode components
for each of the node's children.

When rendering recursively, avoid coding patterns that result in infinite recursion:

Don't recursively render a data structure that contains a cycle. For example, don't
render a tree node whose children includes itself.
Don't create a chain of layouts that contain a cycle. For example, don't create a
layout whose layout is itself.
Don't allow an end user to violate recursion invariants (rules) through malicious
data entry or JavaScript interop calls.

Infinite loops during rendering:


Causes the rendering process to continue forever.
Is equivalent to creating an unterminated loop.

In these scenarios, the Blazor WebAssembly thread or Blazor Server circuit fails and
usually attempts to:

Consume as much CPU time as permitted by the operating system, indefinitely.


Consume an unlimited amount of memory. Consuming unlimited memory is
equivalent to the scenario where an unterminated loop adds entries to a collection
on every iteration.

To avoid infinite recursion patterns, ensure that recursive rendering code contains
suitable stopping conditions.

Custom render tree logic


Most Razor components are implemented as Razor component files ( .razor ) and are
compiled by the framework to produce logic that operates on a RenderTreeBuilder to
render their output. However, a developer may manually implement RenderTreeBuilder
logic using procedural C# code. For more information, see ASP.NET Core Blazor
advanced scenarios (render tree construction).

2 Warning

Use of manual render tree builder logic is considered an advanced and unsafe
scenario, not recommended for general component development.

If RenderTreeBuilder code is written, the developer must guarantee the correctness of


the code. For example, the developer must ensure that:

Calls to OpenElement and CloseElement are correctly balanced.


Attributes are only added in the correct places.

Incorrect manual render tree builder logic can cause arbitrary undefined behavior,
including crashes, app (Blazor WebAssembly) or server (Blazor Server) hangs, and
security vulnerabilities.

Consider manual render tree builder logic on the same level of complexity and with the
same level of danger as writing assembly code or Microsoft Intermediate Language
(MSIL) instructions by hand.

Additional resources
ASP.NET Core Blazor logging
Handle errors in ASP.NET Core†
Create web APIs with ASP.NET Core
Blazor samples GitHub repository (dotnet/blazor-samples)

†Applies to backend ASP.NET Core web API apps that client-side Blazor WebAssembly
apps use for logging.
ASP.NET Core Blazor SignalR guidance
Article • 06/16/2023

This article explains how to configure and manage SignalR connections in Blazor apps.

For general guidance on ASP.NET Core SignalR configuration, see the topics in the
Overview of ASP.NET Core SignalR area of the documentation. To configure SignalR
added to a hosted Blazor WebAssembly app, for example in the Use ASP.NET Core
SignalR with Blazor tutorial, or a standalone Blazor WebAssembly app that uses SignalR,
see ASP.NET Core SignalR configuration.

Disable response compression for Hot Reload


When using Hot Reload, disable Response Compression Middleware in the Development
environment. The following examples use the existing environment check in a project
created from a Blazor project template. Whether or not the default code from a project
template is used, always call UseResponseCompression first in the request processing
pipeline.

In Program.cs of a Blazor Server app:

C#

if (!app.Environment.IsDevelopment())
{
app.UseResponseCompression();
app.UseExceptionHandler("/Error");
app.UseHsts();
}

In Program.cs of the Client project in a hosted Blazor WebAssembly solution:

C#

if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseResponseCompression();
app.UseExceptionHandler("/Error");
app.UseHsts();
}
SignalR cross-origin negotiation for
authentication (Blazor WebAssembly)
This section explains how to configure SignalR's underlying client to send credentials,
such as cookies or HTTP authentication headers.

Use SetBrowserRequestCredentials to set Include on cross-origin fetch requests.

IncludeRequestCredentialsMessageHandler.cs :

C#

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Http;

public class IncludeRequestCredentialsMessageHandler : DelegatingHandler


{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{

request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
return base.SendAsync(request, cancellationToken);
}
}

Where a hub connection is built, assign the HttpMessageHandler to the


HttpMessageHandlerFactory option:

C#

private HubConnectionBuilder? hubConnection;

...

hubConnection = new HubConnectionBuilder()


.WithUrl(new Uri(Navigation.ToAbsoluteUri("/chathub")), options =>
{
options.HttpMessageHandlerFactory = innerHandler =>
new IncludeRequestCredentialsMessageHandler { InnerHandler =
innerHandler };
}).Build();

The preceding example configures the hub connection URL to the absolute URI address
at /chathub , which is the URL used in the SignalR with Blazor tutorial in the Index
component ( Pages/Index.razor ). The URI can also be set via a string, for example
https://signalr.example.com , or via configuration. Navigation is an injected
NavigationManager.

For more information, see ASP.NET Core SignalR configuration.

Render mode (Blazor WebAssembly)


If a Blazor WebAssembly app that uses SignalR is configured to prerender on the server,
prerendering occurs before the client connection to the server is established. For more
information, see the following articles:

Component Tag Helper in ASP.NET Core


Prerender and integrate ASP.NET Core Razor components

Additional resources for Blazor WebAssembly


apps
Hosted Blazor WebAssembly: Secure a SignalR hub
Host and deploy ASP.NET Core Blazor WebAssembly
Overview of ASP.NET Core SignalR
ASP.NET Core SignalR configuration
Blazor samples GitHub repository (dotnet/blazor-samples)

Use sticky sessions for webfarm hosting (Blazor


Server)
A Blazor Server app prerenders in response to the first client request, which creates UI
state on the server. When the client attempts to create a SignalR connection, the client
must reconnect to the same server. Blazor Server apps that use more than one backend
server should implement sticky sessions for SignalR connections.

7 Note

The following error is thrown by an app that hasn't enabled sticky sessions in a
webfarm:

blazor.server.js:1 Uncaught (in promise) Error: Invocation canceled due to the


underlying connection being closed.
Azure SignalR Service (Blazor Server)
We recommend using the Azure SignalR Service for Blazor Server apps hosted in
Microsoft Azure. The service works in conjunction with the app's Blazor Hub for scaling
up a Blazor Server app to a large number of concurrent SignalR connections. In addition,
the SignalR Service's global reach and high-performance data centers significantly aid in
reducing latency due to geography.

Sticky sessions are enabled for the Azure SignalR Service by setting the service's
ServerStickyMode option or configuration value to Required . For more information, see

Host and deploy ASP.NET Core Blazor Server.

Circuit handler options for Blazor Server apps


Configure the Blazor Server circuit with the CircuitOptions shown in the following table.

Option Default Description

DetailedErrors false Send detailed exception messages to


JavaScript when an unhandled
exception occurs on the circuit or when
a .NET method invocation through JS
interop results in an exception.

DisconnectedCircuitMaxRetained 100 Maximum number of disconnected


circuits that the server holds in memory
at a time.

DisconnectedCircuitRetentionPeriod 3 Maximum amount of time a


minutes disconnected circuit is held in memory
before being torn down.

JSInteropDefaultCallTimeout 1 Maximum amount of time the server


minute waits before timing out an
asynchronous JavaScript function
invocation.
Option Default Description

MaxBufferedUnacknowledgedRenderBatches 10 Maximum number of unacknowledged


render batches the server keeps in
memory per circuit at a given time to
support robust reconnection. After
reaching the limit, the server stops
producing new render batches until
one or more batches are acknowledged
by the client.

Configure the options in Program.cs with an options delegate to AddServerSideBlazor.


The following example assigns the default option values shown in the preceding table.
Confirm that Program.cs uses the System namespace ( using System; ).

In Program.cs :

C#

builder.Services.AddServerSideBlazor(options =>
{
options.DetailedErrors = false;
options.DisconnectedCircuitMaxRetained = 100;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3);
options.JSInteropDefaultCallTimeout = TimeSpan.FromMinutes(1);
options.MaxBufferedUnacknowledgedRenderBatches = 10;
});

To configure the HubConnectionContext, use HubConnectionContextOptions with


AddHubOptions. For option descriptions, see ASP.NET Core SignalR configuration. The
following example assigns the default option values. Confirm that the file uses the
System namespace ( using System; ).

In Program.cs :

C#

builder.Services.AddServerSideBlazor()
.AddHubOptions(options =>
{
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
options.EnableDetailedErrors = false;
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.MaximumParallelInvocationsPerClient = 1;
options.MaximumReceiveMessageSize = 32 * 1024;
options.StreamBufferCapacity = 10;
});
2 Warning

The default value of MaximumReceiveMessageSize is 32 KB. Increasing the value


may increase the risk of Denial of service (DoS) attacks.

For information on Blazor Server's memory model, see Host and deploy ASP.NET Core
Blazor Server.

Maximum receive message size


This section only applies to Blazor Server apps and hosted Blazor WebAssembly solutions
that implement SignalR.

The maximum incoming SignalR message size permitted for hub methods is limited by
the HubOptions.MaximumReceiveMessageSize (default: 32 KB). SignalR messages larger
than MaximumReceiveMessageSize throw an error. The framework doesn't impose a
limit on the size of a SignalR message from the hub to a client.

When SignalR logging isn't set to Debug or Trace, a message size error only appears in
the browser's developer tools console:

Error: Connection disconnected with error 'Error: Server returned an error on close:
Connection closed with an error.'.

When SignalR server-side logging is set to Debug or Trace, server-side logging surfaces
an InvalidDataException for a message size error.

appsettings.Development.json :

JSON

{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
...
"Microsoft.AspNetCore.SignalR": "Debug"
}
}
}

Error:
System.IO.InvalidDataException: The maximum message size of 32768B was
exceeded. The message size can be configured in AddHubOptions.

One approach involves increasing the limit by setting MaximumReceiveMessageSize in


Program.cs . The following example sets the maximum receive message size to 64 KB:

C#

builder.Services.AddServerSideBlazor()
.AddHubOptions(options => options.MaximumReceiveMessageSize = 64 *
1024);

Increasing the SignalR incoming message size limit comes at the cost of requiring more
server resources, and it increases the risk of Denial of service (DoS) attacks. Additionally,
reading a large amount of content in to memory as strings or byte arrays can also result
in allocations that work poorly with the garbage collector, resulting in additional
performance penalties.

A better option for reading large payloads is to send the content in smaller chunks and
process the payload as a Stream. This can be used when reading large JavaScript (JS)
interop JSON payloads or if JS interop data is available as raw bytes. For an example that
demonstrates sending large binary payloads in Blazor Server that uses techniques
similar to the InputFile component, see the Binary Submit sample app and the Blazor
InputLargeTextArea Component Sample .

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Forms that process large payloads over SignalR can also use streaming JS interop
directly. For more information, see Call .NET methods from JavaScript functions in
ASP.NET Core Blazor. For a forms example that streams <textarea> data in a Blazor
Server app, see ASP.NET Core Blazor forms and input components.

Consider the following guidance when developing code that transfers a large amount of
data:
Leverage the native streaming JS interop support to transfer data larger than the
SignalR incoming message size limit:
Call JavaScript functions from .NET methods in ASP.NET Core Blazor
Call .NET methods from JavaScript functions in ASP.NET Core Blazor
Form payload example: ASP.NET Core Blazor forms and input components
General tips:
Don't allocate large objects in JS and C# code.
Free consumed memory when the process is completed or cancelled.
Enforce the following additional requirements for security purposes:
Declare the maximum file or data size that can be passed.
Declare the minimum upload rate from the client to the server.
After the data is received by the server, the data can be:
Temporarily stored in a memory buffer until all of the segments are collected.
Consumed immediately. For example, the data can be stored immediately in
a database or written to disk as each segment is received.

Blazor Hub endpoint route configuration


(Blazor Server)
In Program.cs , Blazor Server apps call MapBlazorHub to map the Blazor Hub to the
app's default path. The Blazor Server script ( blazor.server.js ) automatically points to
the endpoint created by MapBlazorHub.

Reflect the connection state in the UI (Blazor


Server)
When the client detects that the connection has been lost, a default UI is displayed to
the user while the client attempts to reconnect. If reconnection fails, the user is provided
the option to retry.

To customize the UI, define a single element with an id of components-reconnect-modal .


The following example places the element in the host page.

Pages/_Host.cshtml :

CSHTML

<div id="components-reconnect-modal">
There was a problem with the connection!
</div>
7 Note

If more than one element with an id of components-reconnect-modal are rendered


by the app, only the first rendered element receives CSS class changes to display or
hide the element.

Add the following CSS styles to the site's stylesheet.

wwwroot/css/site.css :

css

#components-reconnect-modal {
display: none;
}

#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected {
display: block;
}

The following table describes the CSS classes applied to the components-reconnect-
modal element by the Blazor framework.

CSS class Indicates…

components- A lost connection. The client is attempting to reconnect. Show the modal.
reconnect-
show

components- An active connection is re-established to the server. Hide the modal.


reconnect-
hide

components- Reconnection failed, probably due to a network failure. To attempt reconnection,


reconnect- call window.Blazor.reconnect() in JavaScript.
failed
CSS class Indicates…

components- Reconnection rejected. The server was reached but refused the connection, and the
reconnect- user's state on the server is lost. To reload the app, call location.reload() in
rejected JavaScript. This connection state may result when:

A crash in the server-side circuit occurs.


The client is disconnected long enough for the server to drop the user's
state. Instances of the user's components are disposed.
The server is restarted, or the app's worker process is recycled.

Customize the delay before the reconnection display appears by setting the transition-
delay property in the site's CSS for the modal element. The following example sets the

transition delay from 500 ms (default) to 1,000 ms (1 second).

wwwroot/css/site.css :

css

#components-reconnect-modal {
transition: visibility 0s linear 1000ms;
}

To display the current reconnect attempt, define an element with an id of components-


reconnect-current-attempt . To display the maximum number of reconnect retries, define
an element with an id of components-reconnect-max-retries . The following example
places these elements inside a reconnect attempt modal element following the previous
example.

CSHTML

<div id="components-reconnect-modal">
There was a problem with the connection!
(Current reconnect attempt:
<span id="components-reconnect-current-attempt"></span> /
<span id="components-reconnect-max-retries"></span>)
</div>

When the custom reconnect modal appears, it renders content similar to the following
based on the preceding code:

HTML

There was a problem with the connection! (Current reconnect attempt: 3 / 8)


Render mode (Blazor Server)
By default, Blazor Server apps prerender the UI on the server before the client
connection to the server is established. For more information, see Component Tag
Helper in ASP.NET Core.

Blazor startup
Configure the manual start of a Blazor app's SignalR circuit in the Pages/_Host.cshtml
file (Blazor Server, all versions except ASP.NET Core 6.0), Pages/_Layout.cshtml file
(Blazor Server, ASP.NET Core 6.0), or wwwroot/index.html (hosted Blazor WebAssembly
with SignalR implemented):

Add an autostart="false" attribute to the <script> tag for the blazor.


{server|webassembly}.js script.
Place a script that calls Blazor.start() after the Blazor script is loaded and inside
the closing </body> tag.

When autostart is disabled, any aspect of the app that doesn't depend on the circuit
works normally. For example, client-side routing is operational. However, any aspect that
depends on the circuit isn't operational until Blazor.start() is called. App behavior is
unpredictable without an established circuit. For example, component methods fail to
execute while the circuit is disconnected.

For more information, including how to initialize Blazor when the document is ready and
how to chain to a JS Promise , see ASP.NET Core Blazor startup.

Configure SignalR timeouts and Keep-Alive on


the client
Configure the following values for the client:

serverTimeoutInMilliseconds : The server timeout in milliseconds. If this timeout

elapses without receiving any messages from the server, the connection is
terminated with an error. The default timeout value is 30 seconds. The server
timeout should be at least double the value assigned to the Keep-Alive interval
( keepAliveIntervalInMilliseconds ).
keepAliveIntervalInMilliseconds : Default interval at which to ping the server. This

setting allows the server to detect hard disconnects, such as when a client unplugs
their computer from the network. The ping occurs at most as often as the server
pings. If the server pings every five seconds, assigning a value lower than 5000 (5
seconds) pings every five seconds. The default value is 15 seconds. The Keep-Alive
interval should be less than or equal to half the value assigned to the server
timeout ( serverTimeoutInMilliseconds ).

The following example for the Pages/_Host.cshtml file (Blazor Server, all versions except
ASP.NET Core 6.0), Pages/_Layout.cshtml file (Blazor Server, ASP.NET Core 6.0), or
wwwroot/index.html (Blazor WebAssembly) shows the assignment of default values:

HTML

<script src="_framework/blazor.{HOSTING MODEL}.js" autostart="false">


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

In the preceding markup, the {HOSTING MODEL} placeholder is either server for a Blazor
Server app or webassembly for a Blazor WebAssembly app.

When creating a hub connection in a component, set the ServerTimeout (default: 30


seconds), HandshakeTimeout (default: 15 seconds), and KeepAliveInterval (default: 15
seconds) on the built HubConnection. The following example, based on the Index
component in the SignalR with Blazor tutorial, shows the assignment of default values:

C#

protected override async Task OnInitializedAsync()


{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.Build();

hubConnection.ServerTimeout = TimeSpan.FromSeconds(30);
hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(15);
hubConnection.KeepAliveInterval = TimeSpan.FromSeconds(15);

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


...
await hubConnection.StartAsync();
}

When changing the values of the server timeout (ServerTimeout) or the Keep-Alive
interval (KeepAliveInterval:

The server timeout should be at least double the value assigned to the Keep-Alive
interval.
The Keep-Alive interval should be less than or equal to half the value assigned to
the server timeout.

For more information, see the Global deployment and connection failures sections of the
following articles:

Host and deploy ASP.NET Core Blazor Server


Host and deploy ASP.NET Core Blazor WebAssembly

Modify the reconnection handler (Blazor


Server)
The reconnection handler's circuit connection events can be modified for custom
behaviors, such as:

To notify the user if the connection is dropped.


To perform logging (from the client) when a circuit is connected.

To modify the connection events, register callbacks for the following connection
changes:

Dropped connections use onConnectionDown .


Established/re-established connections use onConnectionUp .

Both onConnectionDown and onConnectionUp must be specified.

Pages/_Host.cshtml :

CSHTML

<body>
...

<script src="_framework/blazor.server.js" autostart="false"></script>


<script>
Blazor.start({
reconnectionHandler: {
onConnectionDown: (options, error) => console.error(error),
onConnectionUp: () => console.log("Up, up, and away!")
}
});
</script>
</body>

Automatically refresh the page when reconnection fails


(Blazor Server)
The default reconnection behavior requires the user to take manual action to refresh the
page after reconnection fails. However, a custom reconnection handler can be used to
automatically refresh the page:

Pages/_Host.cshtml :

CSHTML

<body>
...

<div id="reconnect-modal" style="display: none;"></div>


<script src="_framework/blazor.server.js" autostart="false"></script>
<script src="boot.js"></script>
</body>

wwwroot/boot.js :

JavaScript

(() => {
const maximumRetryCount = 3;
const retryIntervalMilliseconds = 5000;
const reconnectModal = document.getElementById('reconnect-modal');

const startReconnectionProcess = () => {


reconnectModal.style.display = 'block';

let isCanceled = false;

(async () => {
for (let i = 0; i < maximumRetryCount; i++) {
reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of
${maximumRetryCount}`;

await new Promise(resolve => setTimeout(resolve,


retryIntervalMilliseconds));
if (isCanceled) {
return;
}

try {
const result = await Blazor.reconnect();
if (!result) {
// The server was reached, but the connection was rejected;
reload the page.
location.reload();
return;
}

// Successfully reconnected to the server.


return;
} catch {
// Didn't reach the server; try again.
}
}

// Retried too many times; reload the page.


location.reload();
})();

return {
cancel: () => {
isCanceled = true;
reconnectModal.style.display = 'none';
},
};
};

let currentReconnectionProcess = null;

Blazor.start({
reconnectionHandler: {
onConnectionDown: () => currentReconnectionProcess ??=
startReconnectionProcess(),
onConnectionUp: () => {
currentReconnectionProcess?.cancel();
currentReconnectionProcess = null;
},
},
});
})();

For more information on Blazor startup, see ASP.NET Core Blazor startup.

Adjust the reconnection retry count and


interval (Blazor Server)
To adjust the reconnection retry count and interval, set the number of retries
( maxRetries ) and period in milliseconds permitted for each retry attempt
( retryIntervalMilliseconds ).

Pages/_Host.cshtml :

CSHTML

<body>
...

<script src="_framework/blazor.server.js" autostart="false"></script>


<script>
Blazor.start({
reconnectionOptions: {
maxRetries: 3,
retryIntervalMilliseconds: 2000
}
});
</script>
</body>

For more information on Blazor startup, see ASP.NET Core Blazor startup.

Disconnect the Blazor circuit from the client


(Blazor Server)
By default, a Blazor circuit is disconnected when the unload page event is triggered.
To disconnect the circuit for other scenarios on the client, invoke Blazor.disconnect in
the appropriate event handler. In the following example, the circuit is disconnected
when the page is hidden (pagehide event ):

JavaScript

window.addEventListener('pagehide', () => {
Blazor.disconnect();
});

For more information on Blazor startup, see ASP.NET Core Blazor startup.

Blazor Server circuit handler


Blazor Server allows code to define a circuit handler, which allows running code on
changes to the state of a user's circuit. A circuit handler is implemented by deriving from
CircuitHandler and registering the class in the app's service container. The following
example of a circuit handler tracks open SignalR connections.

TrackingCircuitHandler.cs :

C#

using Microsoft.AspNetCore.Components.Server.Circuits;

public class TrackingCircuitHandler : CircuitHandler


{
private HashSet<Circuit> circuits = new();

public override Task OnConnectionUpAsync(Circuit circuit,


CancellationToken cancellationToken)
{
circuits.Add(circuit);

return Task.CompletedTask;
}

public override Task OnConnectionDownAsync(Circuit circuit,


CancellationToken cancellationToken)
{
circuits.Remove(circuit);

return Task.CompletedTask;
}

public int ConnectedCircuits => circuits.Count;


}

Circuit handlers are registered using DI. Scoped instances are created per instance of a
circuit. Using the TrackingCircuitHandler in the preceding example, a singleton service
is created because the state of all circuits must be tracked.

In Program.cs :

C#

builder.Services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();

If a custom circuit handler's methods throw an unhandled exception, the exception is


fatal to the Blazor Server circuit. To tolerate exceptions in a handler's code or called
methods, wrap the code in one or more try-catch statements with error handling and
logging.
When a circuit ends because a user has disconnected and the framework is cleaning up
the circuit state, the framework disposes of the circuit's DI scope. Disposing the scope
disposes any circuit-scoped DI services that implement System.IDisposable. If any DI
service throws an unhandled exception during disposal, the framework logs the
exception. For more information, see ASP.NET Core Blazor dependency injection.

Blazor Server circuit handler to capture users


for custom services
Use a CircuitHandler to capture a user from the AuthenticationStateProvider and set that
user in a service. For more information and example code, see ASP.NET Core Blazor
Server additional security scenarios.

Avoid IHttpContextAccessor / HttpContext in


Razor components
Don't use IHttpContextAccessor/HttpContext directly or indirectly in the Razor
components of Blazor Server apps. Blazor apps run outside of the ASP.NET Core
pipeline context. The HttpContext isn't guaranteed to be available within the
IHttpContextAccessor, and HttpContext isn't guaranteed to hold the context that started
the Blazor app.

The recommended approach for passing request state to the Blazor app is through root
component parameters during the app's initial rendering. Alternatively, the app can
copy the data into a scoped service in the root component's initialization lifecycle event
for use across the app. For more information, see ASP.NET Core Blazor Server additional
security scenarios.

A critical aspect of Blazor Server security is that the user attached to a given circuit
might become updated at some point after the Blazor circuit is established but the
IHttpContextAccessor isn't updated. For more information on addressing this situation
with custom services, see ASP.NET Core Blazor Server additional security scenarios.

Additional resources for Blazor Server apps


Blazor Server host and deployment guidance: SignalR configuration
Overview of ASP.NET Core SignalR
ASP.NET Core SignalR configuration
Blazor Server security documentation
ASP.NET Core Blazor authentication and authorization
Secure ASP.NET Core Blazor Server apps
Threat mitigation guidance for ASP.NET Core Blazor Server
ASP.NET Core Blazor Server additional security scenarios
Blazor Server reconnection events and component lifecycle events
What is Azure SignalR Service?
Performance guide for Azure SignalR Service
Publish an ASP.NET Core SignalR app to Azure App Service
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor static files
Article • 04/04/2023

This article describes the configuration for serving static files in Blazor apps.

For more information on solutions in sections that apply to hosted Blazor WebAssembly
apps, see Tooling for ASP.NET Core Blazor.

Static File Middleware


This section applies to Blazor Server apps and the Server app of a hosted Blazor
WebAssembly solution.

Configure Static File Middleware to serve static assets to clients by calling UseStaticFiles
in the app's request processing pipeline. For more information, see Static files in
ASP.NET Core.

Static files in non- Development environments


for Blazor Server apps
This section applies to Blazor Server apps.

In Blazor Server apps run locally, static web assets are only enabled by default in the
Development environment. To enable static files for environments other than
Development during local development and testing (for example, Staging), call
UseStaticWebAssets on the WebApplicationBuilder in Program.cs .

2 Warning

Call UseStaticWebAssets for the exact environment to prevent activating the


feature in production, as it serves files from separate locations on disk other than
from the project if called in a production environment. The example in this section
checks for the Staging environment by calling IsStaging.

C#

if (builder.Environment.IsStaging())
{
builder.WebHost.UseStaticWebAssets();
}
Static web asset base path
This section applies to standalone Blazor WebAssembly apps and hosted Blazor
WebAssembly solutions.

By default, publishing a Blazor WebAssembly app places the app's static assets,
including Blazor framework files ( _framework folder assets), at the root path ( / ) in
published output. The <StaticWebAssetBasePath> property specified in the project file
( .csproj ) sets the base path to a non-root path:

XML

<PropertyGroup>
<StaticWebAssetBasePath>{PATH}</StaticWebAssetBasePath>
</PropertyGroup>

In the preceding example, the {PATH} placeholder is the path.

The <StaticWebAssetBasePath> property is most commonly used to control the paths to


published static assets of multiple Blazor WebAssembly apps in a single hosted
deployment. For more information, see Multiple hosted ASP.NET Core Blazor
WebAssembly apps. The property is also effective in standalone Blazor WebAssembly
apps.

Without setting the <StaticWebAssetBasePath> property, the client app of a hosted


solution or a standalone app is published at the following paths:

In the Server project of a hosted Blazor WebAssembly solution:


/BlazorHostedSample/Server/bin/Release/{TFM}/publish/wwwroot/

In a standalone Blazor WebAssembly app:


/BlazorStandaloneSample/bin/Release/{TFM}/publish/wwwroot/

In the preceding examples, the {TFM} placeholder is the Target Framework Moniker
(TFM) (for example, net6.0 ).

If the <StaticWebAssetBasePath> property in the Client project of a hosted Blazor


WebAssembly app or in a standalone Blazor WebAssembly app sets the published static
asset path to app1 , the root path to the app in published output is /app1 .

In the Client app's project file ( .csproj ) or the standalone Blazor WebAssembly app's
project file ( .csproj ):

XML
<PropertyGroup>
<StaticWebAssetBasePath>app1</StaticWebAssetBasePath>
</PropertyGroup>

In published output:

Path to the client app in the Server project of a hosted Blazor WebAssembly
solution: /BlazorHostedSample/Server/bin/Release/{TFM}/publish/wwwroot/app1/
Path to a standalone Blazor WebAssembly app:
/BlazorStandaloneSample/bin/Release/{TFM}/publish/wwwroot/app1/

In the preceding examples, the {TFM} placeholder is the Target Framework Moniker
(TFM) (for example, net6.0 ).

Blazor Server file mappings and static file


options
To create additional file mappings with a FileExtensionContentTypeProvider or configure
other StaticFileOptions, use one of the following approaches. In the following examples,
the {EXTENSION} placeholder is the file extension, and the {CONTENT TYPE} placeholder is
the content type.

Configure options through dependency injection (DI) in Program.cs using


StaticFileOptions:

C#

using Microsoft.AspNetCore.StaticFiles;

...

var provider = new FileExtensionContentTypeProvider();


provider.Mappings["{EXTENSION}"] = "{CONTENT TYPE}";

builder.Services.Configure<StaticFileOptions>(options =>
{
options.ContentTypeProvider = provider;
});

Because this approach configures the same file provider used to serve
blazor.server.js , make sure that your custom configuration doesn't interfere with

serving blazor.server.js . For example, don't remove the mapping for JavaScript
files by configuring the provider with provider.Mappings.Remove(".js") .
Use two calls to UseStaticFiles in Program.cs :
Configure the custom file provider in the first call with StaticFileOptions.
The second middleware serves blazor.server.js , which uses the default static
files configuration provided by the Blazor framework.

C#

using Microsoft.AspNetCore.StaticFiles;

...

var provider = new FileExtensionContentTypeProvider();


provider.Mappings["{EXTENSION}"] = "{CONTENT TYPE}";

app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider =


provider });
app.UseStaticFiles();

You can avoid interfering with serving _framework/blazor.server.js by using


MapWhen to execute a custom Static File Middleware:

C#

app.MapWhen(ctx => !ctx.Request.Path


.StartsWithSegments("/_framework/blazor.server.js"),
subApp => subApp.UseStaticFiles(new StaticFileOptions() { ...
}));

Additional resources
App base path
Multiple hosted ASP.NET Core Blazor WebAssembly apps
ASP.NET Core Razor components
Article • 05/15/2023

This article explains how to create and use Razor components in Blazor apps, including
guidance on Razor syntax, component naming, namespaces, and component
parameters.

Blazor apps are built using Razor components, informally known as Blazor components. A
component is a self-contained portion of user interface (UI) with processing logic to
enable dynamic behavior. Components can be nested, reused, shared among projects,
and used in MVC and Razor Pages apps.

Component classes
Components are implemented using a combination of C# and HTML markup in Razor
component files with the .razor file extension.

By default, ComponentBase is the base class for components described by Razor


component files. ComponentBase implements the lowest abstraction of components,
the IComponent interface. ComponentBase defines component properties and methods
for basic functionality, for example, to process a set of built-in component lifecycle
events.

ComponentBase in dotnet/aspnetcore reference source : The reference source


contains additional remarks on the built-in lifecycle events. However, keep in mind that
the internal implementations of component features are subject to change at any time
without notice.

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Developers typically create Razor components from Razor component files ( .razor ) or
base their components on ComponentBase, but components can also be built by
implementing IComponent. Developer-built components that implement IComponent
can take low-level control over rendering at the cost of having to manually trigger
rendering with events and lifecycle methods that the developer must create and
maintain.

Razor syntax
Components use Razor syntax. Two Razor features are extensively used by components,
directives and directive attributes. These are reserved keywords prefixed with @ that
appear in Razor markup:

Directives: Change the way component markup is parsed or functions. For


example, the @page directive specifies a routable component with a route
template and can be reached directly by a user's request in the browser at a
specific URL.
Directive attributes: Change the way a component element is parsed or functions.
For example, the @bind directive attribute for an <input> element binds data to
the element's value.

Directives and directive attributes used in components are explained further in this
article and other articles of the Blazor documentation set. For general information on
Razor syntax, see Razor syntax reference for ASP.NET Core.

Component name, class name, and namespace


A component's name must start with an uppercase character:

✔️ ProductDetail.razor

❌ productDetail.razor

Common Blazor naming conventions used throughout the Blazor documentation


include:

Component file paths use Pascal case† and appear before showing component
code examples. Paths indicate typical folder locations. For example,
Pages/ProductDetail.razor indicates that the ProductDetail component has a file
name of ProductDetail.razor and resides in the Pages folder of the app.
Component file paths for routable components match their URLs in kebab case‡
with hyphens appearing between words in a component's route template. For
example, a ProductDetail component with a route template of /product-detail
( @page "/product-detail" ) is requested in a browser at the relative URL /product-
detail .
†Pascal case (upper camel case) is a naming convention without spaces and punctuation
and with the first letter of each word capitalized, including the first word.
‡kebab case is a naming convention without spaces and punctuation that uses
lowercase letters and dashes between words.

Components are ordinary C# classes and can be placed anywhere within a project.
Components that produce webpages usually reside in the Pages folder. Non-page
components are frequently placed in the Shared folder or a custom folder added to the
project.

Typically, a component's namespace is derived from the app's root namespace and the
component's location (folder) within the app. If the app's root namespace is
BlazorSample and the Counter component resides in the Pages folder:

The Counter component's namespace is BlazorSample.Pages .


The fully qualified type name of the component is BlazorSample.Pages.Counter .

For custom folders that hold components, add an @using directive to the parent
component or to the app's _Imports.razor file. The following example makes
components in the Components folder available:

razor

@using BlazorSample.Components

7 Note

@using directives in the _Imports.razor file are only applied to Razor files
( .razor ), not C# files ( .cs ).

Components can also be referenced using their fully qualified names, which doesn't
require an @using directive. The following example directly references the
ProductDetail component in the Components folder of the app:

razor

<BlazorSample.Components.ProductDetail />

The namespace of a component authored with Razor is based on the following (in
priority order):
The @namespace directive in the Razor file's markup (for example, @namespace
BlazorSample.CustomNamespace ).
The project's RootNamespace in the project file (for example,
<RootNamespace>BlazorSample</RootNamespace> ).
The project name, taken from the project file's file name ( .csproj ), and the path
from the project root to the component. For example, the framework resolves
{PROJECT ROOT}/Pages/Index.razor with a project namespace of BlazorSample
( BlazorSample.csproj ) to the namespace BlazorSample.Pages for the Index
component. {PROJECT ROOT} is the project root path. Components follow C# name
binding rules. For the Index component in this example, the components in scope
are all of the components:
In the same folder, Pages .
The components in the project's root that don't explicitly specify a different
namespace.

The following are not supported:

The global:: qualification.


Importing components with aliased using statements. For example, @using Foo =
Bar isn't supported.

Partially-qualified names. For example, you can't add @using BlazorSample to a


component and then reference the NavMenu component in the app's Shared folder
( Shared/NavMenu.razor ) with <Shared.NavMenu></Shared.NavMenu> .

Partial class support


Components are generated as C# partial classes and are authored using either of the
following approaches:

A single file contains C# code defined in one or more @code blocks, HTML
markup, and Razor markup. Blazor project templates define their components
using this single-file approach.
HTML and Razor markup are placed in a Razor file ( .razor ). C# code is placed in a
code-behind file defined as a partial class ( .cs ).

7 Note

A component stylesheet that defines component-specific styles is a separate file


( .css ). Blazor CSS isolation is described later in ASP.NET Core Blazor CSS isolation.
The following example shows the default Counter component with an @code block in
an app generated from a Blazor project template. Markup and C# code are in the same
file. This is the most common approach taken in component authoring.

Pages/Counter.razor :

razor

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

The following Counter component splits presentation HTML and Razor markup from the
C# code using a code-behind file with a partial class. Splitting the markup from the C#
code is favored by some organizations and developers to organize their component
code to suit how they prefer to work. For example, the organization's UI expert can work
on the presentation layer independently of another developer working on the
component's C# logic. The approach is also useful when working with automatically-
generated code or source generators. For more information, see Partial Classes and
Methods (C# Programming Guide).

Pages/CounterPartialClass.razor :

razor

@page "/counter-partial-class"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>


<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

Pages/CounterPartialClass.razor.cs :

C#

namespace BlazorSample.Pages;

public partial class CounterPartialClass


{
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

@using directives in the _Imports.razor file are only applied to Razor files ( .razor ), not
C# files ( .cs ). Add namespaces to a partial class file as needed.

Typical namespaces used by components:

C#

using System.Net.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.JSInterop;

Typical namespaces also include the namespace of the app and the namespace
corresponding to the app's Shared folder:

C#

using BlazorSample;
using BlazorSample.Shared;

Specify a base class


The @inherits directive is used to specify a base class for a component. Unlike using
partial classes, which only split markup from C# logic, using a base class allows you to
inherit C# code for use across a group of components that share the base class's
properties and methods. Using base classes reduce code redundancy in apps and are
useful when supplying base code from class libraries to multiple apps. For more
information, see Inheritance in C# and .NET.

In the following example, the BlazorRocksBase base class derives from ComponentBase.

Pages/BlazorRocks.razor :

razor

@page "/blazor-rocks"
@inherits BlazorRocksBase

<h1>@BlazorRocksText</h1>

BlazorRocksBase.cs :

C#

using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase : ComponentBase


{
public string BlazorRocksText { get; set; } =
"Blazor rocks the browser!";
}

Routing
Routing in Blazor is achieved by providing a route template to each accessible
component in the app with an @page directive. When a Razor file with an @page
directive is compiled, the generated class is given a RouteAttribute specifying the route
template. At runtime, the router searches for component classes with a RouteAttribute
and renders whichever component has a route template that matches the requested
URL.

The following HelloWorld component uses a route template of /hello-world , and the
rendered webpage for the component is reached at the relative URL /hello-world .
Components that produce webpages usually reside in the Pages folder, but you can use
any folder to hold components, including within nested folders.

Pages/HelloWorld.razor :

razor

@page "/hello-world"

<h1>Hello World!</h1>

The preceding component loads in the browser at /hello-world regardless of whether


or not you add the component to the app's UI navigation. Optionally, components can
be added to the NavMenu component so that a link to the component appears in the
app's UI-based navigation.

For the preceding HelloWorld component, you can add a NavLink component to the
NavMenu component in the Shared folder. For more information, including descriptions
of the NavLink and NavMenu components, see ASP.NET Core Blazor routing and
navigation.

Markup
A component's UI is defined using Razor syntax, which consists of Razor markup, C#,
and HTML. When an app is compiled, the HTML markup and C# rendering logic are
converted into a component class. The name of the generated class matches the name
of the file.

Members of the component class are defined in one or more @code blocks. In @code
blocks, component state is specified and processed with C#:

Property and field initializers.


Parameter values from arguments passed by parent components and route
parameters.
Methods for user event handling, lifecycle events, and custom component logic.

Component members are used in rendering logic using C# expressions that start with
the @ symbol. For example, a C# field is rendered by prefixing @ to the field name. The
following Markup component evaluates and renders:

headingFontStyle for the CSS property value font-style of the heading element.

headingText for the content of the heading element.


Pages/Markup.razor :

razor

@page "/markup"

<h1 style="font-style:@headingFontStyle">@headingText</h1>

@code {
private string headingFontStyle = "italic";
private string headingText = "Put on your new Blazor!";
}

7 Note

Examples throughout the Blazor documentation specify the private access modifier
for private members. Private members are scoped to a component's class. However,
C# assumes the private access modifier when no access modifier is present, so
explicitly marking members " private " in your own code is optional. For more
information on access modifiers, see Access Modifiers (C# Programming Guide).

The Blazor framework processes a component internally as a render tree , which is the
combination of a component's Document Object Model (DOM) and Cascading Style
Sheet Object Model (CSSOM) . After the component is initially rendered, the
component's render tree is regenerated in response to events. Blazor compares the new
render tree against the previous render tree and applies any modifications to the
browser's DOM for display. For more information, see ASP.NET Core Razor component
rendering.

Razor syntax for C# control structures, directives, and directive attributes are lowercase
(examples: @if, @code, @bind). Property names are uppercase (example: @Body for
LayoutComponentBase.Body).

Asynchronous methods ( async ) don't support returning


void

The Blazor framework doesn't track void -returning asynchronous methods ( async ). As a
result, exceptions aren't caught if void is returned. Always return a Task from
asynchronous methods.

Nested components
Components can include other components by declaring them using HTML syntax. The
markup for using a component looks like an HTML tag where the name of the tag is the
component type.

Consider the following Heading component, which can be used by other components to
display a heading.

Shared/Heading.razor :

razor

<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
private string headingFontStyle = "italic";
}

The following markup in the HeadingExample component renders the preceding Heading
component at the location where the <Heading /> tag appears.

Pages/HeadingExample.razor :

razor

@page "/heading-example"

<Heading />

If a component contains an HTML element with an uppercase first letter that doesn't
match a component name within the same namespace, a warning is emitted indicating
that the element has an unexpected name. Adding an @using directive for the
component's namespace makes the component available, which resolves the warning.
For more information, see the Component name, class name, and namespace section.

The Heading component example shown in this section doesn't have an @page
directive, so the Heading component isn't directly accessible to a user via a direct
request in the browser. However, any component with an @page directive can be
nested in another component. If the Heading component was directly accessible by
including @page "/heading" at the top of its Razor file, then the component would be
rendered for browser requests at both /heading and /heading-example .

Component parameters
Component parameters pass data to components and are defined using public C#
properties on the component class with the [Parameter] attribute. In the following
example, a built-in reference type (System.String) and a user-defined reference type
( PanelBody ) are passed as component parameters.

PanelBody.cs :

C#

public class PanelBody


{
public string? Text { get; set; }
public string? Style { get; set; }
}

Shared/ParameterChild.razor :

razor

<div class="card w-25" style="margin-bottom:15px">


<div class="card-header font-weight-bold">@Title</div>
<div class="card-body" style="font-style:@Body.Style">
@Body.Text
</div>
</div>

@code {
[Parameter]
public string Title { get; set; } = "Set By Child";

[Parameter]
public PanelBody Body { get; set; } =
new()
{
Text = "Set by child.",
Style = "normal"
};
}

2 Warning

Providing initial values for component parameters is supported, but don't create a
component that writes to its own parameters after the component is rendered for
the first time. For more information, see Avoid overwriting parameters in ASP.NET
Core Blazor.
The Title and Body component parameters of the ParameterChild component are set
by arguments in the HTML tag that renders the instance of the component. The
following ParameterParent component renders two ParameterChild components:

The first ParameterChild component is rendered without supplying parameter


arguments.
The second ParameterChild component receives values for Title and Body from
the ParameterParent component, which uses an explicit C# expression to set the
values of the PanelBody 's properties.

Pages/ParameterParent.razor :

razor

@page "/parameter-parent"

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent"


Body="@(new PanelBody() { Text = "Set by parent.", Style =
"italic" })" />

The following rendered HTML markup from the ParameterParent component shows
ParameterChild component default values when the ParameterParent component
doesn't supply component parameter values. When the ParameterParent component
provides component parameter values, they replace the ParameterChild component's
default values.

7 Note

For clarity, rendered CSS style classes aren't shown in the following rendered HTML
markup.

HTML

<h1>Child component (without attribute values)</h1>

<div>
<div>Set By Child</div>
<div>Set by child.</div>
</div>
<h1>Child component (with attribute values)</h1>

<div>
<div>Set by Parent</div>
<div>Set by parent.</div>
</div>

Assign a C# field, property, or result of a method to a component parameter as an


HTML attribute value. The value of the attribute can typically be any C# expression that
matches the type of the parameter. The value of the attribute can optionally lead with a
Razor reserved @ symbol, but it isn't required.

If the component parameter is of type string, then the attribute value is instead treated
as a C# string literal by default. If you want to specify a C# expression instead, then use
the @ prefix.

The following ParameterParent2 component displays four instances of the preceding


ParameterChild component and sets their Title parameter values to:

The value of the title field.


The result of the GetTitle C# method.
The current local date in long format with ToLongDateString, which uses an implicit
C# expression.
The panelData object's Title property.

We don't recommend the use of the @ prefix for literals (for example, boolean values),
keywords (for example, this ), or null , but you can choose to use them if you wish. For
example, IsFixed="@true" is uncommon but supported.

Quotes around parameter attribute values are optional in most cases per the HTML5
specification. For example, Value=this is supported, instead of Value="this" . However,
we recommend using quotes because it's easier to remember and widely adopted
across web-based technologies.

Throughout the documentation, code examples:

Always use quotes. Example: Value="this" .


Use the @ prefix with nonliterals, even when it's optional. Example: Count="@ct" ,
where ct is a number-typed variable. Count="ct" is a valid stylistic approach, but
the documentation and examples don't adopt the convention.
Always avoid @ for literals, outside of Razor expressions. Example: IsFixed="true" .

Pages/ParameterParent2.razor :
razor

@page "/parameter-parent-2"

<ParameterChild Title="@title" />

<ParameterChild Title="@GetTitle()" />

<ParameterChild Title="@DateTime.Now.ToLongDateString()" />

<ParameterChild Title="@panelData.Title" />

@code {
private string title = "From Parent field";
private PanelData panelData = new();

private string GetTitle()


{
return "From Parent method";
}

private class PanelData


{
public string Title { get; set; } = "From Parent object";
}
}

7 Note

When assigning a C# member to a component parameter, don't prefix the


parameter's HTML attribute with @ .

Correct ( Title is a string parameter, Count is a number-typed parameter):

razor

<ParameterChild Title="@title" Count="@ct" />

razor

<ParameterChild Title="@title" Count="ct" />

Incorrect:

razor

<ParameterChild @Title="@title" @Count="@ct" />


razor

<ParameterChild @Title="@title" @Count="ct" />

Unlike in Razor pages ( .cshtml ), Blazor can't perform asynchronous work in a Razor
expression while rendering a component. This is because Blazor is designed for
rendering interactive UIs. In an interactive UI, the screen must always display something,
so it doesn't make sense to block the rendering flow. Instead, asynchronous work is
performed during one of the asynchronous lifecycle events. After each asynchronous
lifecycle event, the component may render again. The following Razor syntax is not
supported:

razor

<ParameterChild Title="@await ..." />

The code in the preceding example generates a compiler error when the app is built:

The 'await' operator can only be used within an async method. Consider marking
this method with the 'async' modifier and changing its return type to 'Task'.

To obtain a value for the Title parameter in the preceding example asynchronously, the
component can use the OnInitializedAsync lifecycle event, as the following example
demonstrates:

razor

<ParameterChild Title="@title" />

@code {
private string? title;

protected override async Task OnInitializedAsync()


{
title = await ...;
}
}

For more information, see ASP.NET Core Razor component lifecycle.

Use of an explicit Razor expression to concatenate text with an expression result for
assignment to a parameter is not supported. The following example seeks to
concatenate the text " Set by " with an object's property value. Although this syntax is
supported in a Razor page ( .cshtml ), it isn't valid for assignment to the child's Title
parameter in a component. The following Razor syntax is not supported:

razor

<ParameterChild Title="Set by @(panelData.Title)" />

The code in the preceding example generates a compiler error when the app is built:

Component attributes do not support complex content (mixed C# and markup).

To support the assignment of a composed value, use a method, field, or property. The
following example performs the concatenation of " Set by " and an object's property
value in the C# method GetTitle :

Pages/ParameterParent3.razor :

razor

@page "/parameter-parent-3"

<ParameterChild Title="@GetTitle()" />

@code {
private PanelData panelData = new();

private string GetTitle() => $"Set by {panelData.Title}";

private class PanelData


{
public string Title { get; set; } = "Parent";
}
}

For more information, see Razor syntax reference for ASP.NET Core.

2 Warning

Providing initial values for component parameters is supported, but don't create a
component that writes to its own parameters after the component is rendered for
the first time. For more information, see Avoid overwriting parameters in ASP.NET
Core Blazor.
Component parameters should be declared as auto-properties, meaning that they
shouldn't contain custom logic in their get or set accessors. For example, the following
StartData property is an auto-property:

C#

[Parameter]
public DateTime StartData { get; set; }

Don't place custom logic in the get or set accessor because component parameters
are purely intended for use as a channel for a parent component to flow information to
a child component. If a set accessor of a child component property contains logic that
causes rerendering of the parent component, an infinite rendering loop results.

To transform a received parameter value:

Leave the parameter property as an auto-property to represent the supplied raw


data.
Create a different property or method to supply the transformed data based on
the parameter property.

Override OnParametersSetAsync to transform a received parameter each time new data


is received.

Writing an initial value to a component parameter is supported because initial value


assignments don't interfere with the Blazor's automatic component rendering. The
following assignment of the current local DateTime with DateTime.Now to StartData is
valid syntax in a component:

C#

[Parameter]
public DateTime StartData { get; set; } = DateTime.Now;

After the initial assignment of DateTime.Now, do not assign a value to StartData in


developer code. For more information, see Avoid overwriting parameters in ASP.NET
Core Blazor.

Apply the [EditorRequired] attribute to specify a required component parameter. If a


parameter value isn't provided, editors or build tools may display warnings to the user.
This attribute is only valid on properties also marked with the [Parameter] attribute. The
EditorRequiredAttribute is enforced at design-time and when the app is built. The
attribute isn't enforced at runtime, and it doesn't guarantee a non- null parameter
value.

C#

[Parameter]
[EditorRequired]
public string? Title { get; set; }

Single-line attribute lists are also supported:

C#

[Parameter, EditorRequired]
public string? Title { get; set; }

Don't use the required modifier or init accessor on component parameter properties.
Components are usually instantiated and assigned parameter values using reflection,
which bypasses the guarantees that init and required are designed to make. Instead,
use the [EditorRequired] attribute to specify a required component parameter.

Tuples (API documentation) are supported for component parameters and


RenderFragment types. The following component parameter example passes three
values in a Tuple :

Shared/RenderTupleChild.razor :

C#

<div class="card w-50" style="margin-bottom:15px">


<div class="card-header font-weight-bold"><code>Tuple</code> Card</div>
<div class="card-body">
<ul>
<li>Integer: @Data?.Item1</li>
<li>String: @Data?.Item2</li>
<li>Boolean: @Data?.Item3</li>
</ul>
</div>
</div>

@code {
[Parameter]
public Tuple<int, string, bool>? Data { get; set; }
}

Pages/RenderTupleParent.razor :
C#

@page "/render-tuple-parent"

<h1>Render <code>Tuple</code> Parent</h1>

<RenderTupleChild Data="@data" />

@code {
private Tuple<int, string, bool> data = new(999, "I aim to misbehave.",
true);
}

Only unnamed tuples are supported for C# 7.0 or later in Razor components. Named
tuples support in Razor components is planned for a future ASP.NET Core release. For
more information, see Blazor Transpiler issue with named Tuples (dotnet/aspnetcore
#28982) .

Quote ©2005 Universal Pictures : Serenity (Nathan Fillion )

Route parameters
Components can specify route parameters in the route template of the @page directive.
The Blazor router uses route parameters to populate corresponding component
parameters.

Optional route parameters are supported. In the following example, the text optional
parameter assigns the value of the route segment to the component's Text property. If
the segment isn't present, the value of Text is set to " fantastic " in the OnInitialized
lifecycle method.

Pages/RouteParameter.razor :

razor

@page "/route-parameter/{text?}"

<h1>Blazor is @Text!</h1>

@code {
[Parameter]
public string? Text { get; set; }

protected override void OnInitialized()


{
Text = Text ?? "fantastic";
}
}

For information on catch-all route parameters ( {*pageRoute} ), which capture paths


across multiple folder boundaries, see ASP.NET Core Blazor routing and navigation.

Child content render fragments


Components can set the content of another component. The assigning component
provides the content between the child component's opening and closing tags.

In the following example, the RenderFragmentChild component has a ChildContent


component parameter that represents a segment of the UI to render as a
RenderFragment. The position of ChildContent in the component's Razor markup is
where the content is rendered in the final HTML output.

Shared/RenderFragmentChild.razor :

razor

<div class="card w-25" style="margin-bottom:15px">


<div class="card-header font-weight-bold">Child content</div>
<div class="card-body">@ChildContent</div>
</div>

@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}

) Important

The property receiving the RenderFragment content must be named ChildContent


by convention.

Event callbacks aren't supported for RenderFragment.

The following RenderFragmentParent component provides content for rendering the


RenderFragmentChild by placing the content inside the child component's opening and
closing tags.

Pages/RenderFragmentParent.razor :
razor

@page "/render-fragment-parent"

<h1>Render child content</h1>

<RenderFragmentChild>
Content of the child component is supplied
by the parent component.
</RenderFragmentChild>

Due to the way that Blazor renders child content, rendering components inside a for
loop requires a local index variable if the incrementing loop variable is used in the
RenderFragmentChild component's content. The following example can be added to the
preceding RenderFragmentParent component:

razor

<h1>Three children with an index variable</h1>

@for (int c = 0; c < 3; c++)


{
var current = c;

<RenderFragmentChild>
Count: @current
</RenderFragmentChild>
}

Alternatively, use a foreach loop with Enumerable.Range instead of a for loop. The
following example can be added to the preceding RenderFragmentParent component:

razor

<h1>Second example of three children with an index variable</h1>

@foreach (var c in Enumerable.Range(0,3))


{
<RenderFragmentChild>
Count: @c
</RenderFragmentChild>
}

Render fragments are used to render child content throughout Blazor apps and are
described with examples in the following articles and article sections:

Blazor layouts
Pass data across a component hierarchy
Templated components
Global exception handling

7 Note

Blazor framework's built-in Razor components use the same ChildContent


component parameter convention to set their content. You can see the
components that set child content by searching for the component parameter
property name ChildContent in the API documentation (filters API with the search
term "ChildContent").

Render fragments for reusable rendering logic


You can factor out child components purely as a way of reusing rendering logic. In any
component's @code block, define a RenderFragment and render the fragment from any
location as many times as needed:

razor

<h1>Hello, world!</h1>

@RenderWelcomeInfo

<p>Render the welcome info a second time:</p>

@RenderWelcomeInfo

@code {
private RenderFragment RenderWelcomeInfo = __builder =>
{
<p>Welcome to your new app!</p>
};
}

For more information, see Reuse rendering logic.

Capture references to components


Component references provide a way to reference a component instance for issuing
commands. To capture a component reference:

Add an @ref attribute to the child component.


Define a field with the same type as the child component.

When the component is rendered, the field is populated with the component instance.
You can then invoke .NET methods on the instance.

Consider the following ReferenceChild component that logs a message when its
ChildMethod is called.

Shared/ReferenceChild.razor :

razor

@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> logger

@code {
public void ChildMethod(int value)
{
logger.LogInformation("Received {Value} in ChildMethod", value);
}
}

A component reference is only populated after the component is rendered and its
output includes ReferenceChild 's element. Until the component is rendered, there's
nothing to reference.

To manipulate component references after the component has finished rendering, use
the OnAfterRender or OnAfterRenderAsync methods.

To use a reference variable with an event handler, use a lambda expression or assign the
event handler delegate in the OnAfterRender or OnAfterRenderAsync methods. This
ensures that the reference variable is assigned before the event handler is assigned.

The following lambda approach uses the preceding ReferenceChild component.

Pages/ReferenceParent1.razor :

razor

@page "/reference-parent-1"

<button @onclick="@(() => childComponent?.ChildMethod(5))">


Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>

<ReferenceChild @ref="childComponent" />

@code {
private ReferenceChild? childComponent;
}

The following delegate approach uses the preceding ReferenceChild component.

Pages/ReferenceParent2.razor :

razor

@page "/reference-parent-2"

<button @onclick="@(() => callChildMethod?.Invoke())">


Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>

<ReferenceChild @ref="childComponent" />

@code {
private ReferenceChild? childComponent;
private Action? callChildMethod;

protected override void OnAfterRender(bool firstRender)


{
if (firstRender)
{
callChildMethod = CallChildMethod;
}
}

private void CallChildMethod()


{
childComponent?.ChildMethod(5);
}
}

While capturing component references use a similar syntax to capturing element


references, capturing component references isn't a JavaScript interop feature.
Component references aren't passed to JavaScript code. Component references are only
used in .NET code.

) Important

Do not use component references to mutate the state of child components.


Instead, use normal declarative component parameters to pass data to child
components. Use of component parameters result in child components that
rerender at the correct times automatically. For more information, see the
component parameters section and the ASP.NET Core Blazor data binding article.
Apply an attribute
Attributes can be applied to components with the @attribute directive. The following
example applies the [Authorize] attribute to the component's class:

razor

@page "/"
@attribute [Authorize]

Conditional HTML element attributes


HTML element attribute properties are conditionally set based on the .NET value. If the
value is false or null , the property isn't set. If the value is true , the property is set.

In the following example, IsCompleted determines if the <input> element's checked


property is set.

Pages/ConditionalAttribute.razor :

razor

@page "/conditional-attribute"

<label>
<input type="checkbox" checked="@IsCompleted" />
Is Completed?
</label>

<button @onclick="@(() => IsCompleted = !IsCompleted)">


Change IsCompleted
</button>

@code {
[Parameter]
public bool IsCompleted { get; set; }
}

For more information, see Razor syntax reference for ASP.NET Core.

2 Warning

Some HTML attributes, such as aria-pressed , don't function properly when the
.NET type is a bool . In those cases, use a string type instead of a bool .
Raw HTML
Strings are normally rendered using DOM text nodes, which means that any markup
they may contain is ignored and treated as literal text. To render raw HTML, wrap the
HTML content in a MarkupString value. The value is parsed as HTML or SVG and
inserted into the DOM.

2 Warning

Rendering raw HTML constructed from any untrusted source is a security risk and
should always be avoided.

The following example shows using the MarkupString type to add a block of static
HTML content to the rendered output of a component.

Pages/MarkupStringExample.razor :

razor

@page "/markup-string-example"

@((MarkupString)myMarkup)

@code {
private string myMarkup =
"<p class=\"text-danger\">This is a dangerous <em>markup
string</em>.</p>";
}

Razor templates
Render fragments can be defined using Razor template syntax to define a UI snippet.
Razor templates use the following format:

razor

@<{HTML tag}>...</{HTML tag}>

The following example illustrates how to specify RenderFragment and


RenderFragment<TValue> values and render templates directly in a component. Render
fragments can also be passed as arguments to templated components.

Pages/RazorTemplate.razor :
razor

@page "/razor-template"

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.
</p>;
private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet:
@pet.Name</p>;

private class Pet


{
public string? Name { get; set; }
}
}

Rendered output of the preceding code:

HTML

<p>The time is 4/19/2021 8:54:46 AM.</p>


<p>Pet: Nutty Rex</p>

Static assets
Blazor follows the convention of ASP.NET Core apps for static assets. Static assets are
located in the project's web root (wwwroot) folder or folders under the wwwroot folder.

Use a base-relative path ( / ) to refer to the web root for a static asset. In the following
example, logo.png is physically located in the {PROJECT ROOT}/wwwroot/images folder.
{PROJECT ROOT} is the app's project root.

razor

<img alt="Company logo" src="/images/logo.png" />

Components do not support tilde-slash notation ( ~/ ).

For information on setting an app's base path, see Host and deploy ASP.NET Core
Blazor.
Tag Helpers aren't supported in components
Tag Helpers aren't supported in components. To provide Tag Helper-like functionality in
Blazor, create a component with the same functionality as the Tag Helper and use the
component instead.

Scalable Vector Graphics (SVG) images


Since Blazor renders HTML, browser-supported images, including Scalable Vector
Graphics (SVG) images (.svg) , are supported via the <img> tag:

HTML

<img alt="Example image" src="image.svg" />

Similarly, SVG images are supported in the CSS rules of a stylesheet file ( .css ):

css

.element-class {
background-image: url("image.svg");
}

Blazor supports the <foreignObject> element to display arbitrary HTML within an


SVG. The markup can represent arbitrary HTML, a RenderFragment, or a Razor
component.

The following example demonstrates:

Display of a string ( @message ).


Two-way binding with an <input> element and a value field.
A Robot component.

razor

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">


<rect x="0" y="0" rx="10" ry="10" width="200" height="200"
stroke="black"
fill="none" />
<foreignObject x="20" y="20" width="160" height="160">
<p>@message</p>
</foreignObject>
</svg>

<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject width="200" height="200">
<label>
Two-way binding:
<input @bind="value" @bind:event="oninput" />
</label>
</foreignObject>
</svg>

<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject>
<Robot />
</foreignObject>
</svg>

@code {
private string message = "Lorem ipsum dolor sit amet, consectetur
adipiscing " +
"elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua.";

private string? value;


}

Whitespace rendering behavior


Unless the @preservewhitespace directive is used with a value of true , extra whitespace
is removed by default if:

Leading or trailing within an element.


Leading or trailing within a RenderFragment/RenderFragment<TValue> parameter
(for example, child content passed to another component).
It precedes or follows a C# code block, such as @if or @foreach .

Whitespace removal might affect the rendered output when using a CSS rule, such as
white-space: pre . To disable this performance optimization and preserve the

whitespace, take one of the following actions:

Add the @preservewhitespace true directive at the top of the Razor file ( .razor ) to
apply the preference to a specific component.
Add the @preservewhitespace true directive inside an _Imports.razor file to apply
the preference to a subdirectory or to the entire project.

In most cases, no action is required, as apps typically continue to behave normally (but
faster). If stripping whitespace causes a rendering problem for a particular component,
use @preservewhitespace true in that component to disable this optimization.
Render static root Razor components
A root Razor component is the first component loaded of any component hierarchy
created by the app.

In an app created from the Blazor Server project template, the App component
( App.razor ) is created as the default root component in Pages/_Host.cshtml using the
Component Tag Helper:

CSHTML

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

In an app created from the Blazor WebAssembly project template, the App component
( App.razor ) is created as the default root component in Program.cs :

C#

builder.RootComponents.Add<App>("#app");

In the preceding code, the CSS selector, #app , indicates that the App component is
created for the <div> in wwwroot/index.html with an id of app :

HTML

<div id="app">...</app>

MVC and Razor Pages apps can also use the Component Tag Helper to register
statically-rendered Blazor WebAssembly root components:

CSHTML

<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />

Statically-rendered components can only be added to the app. They can't be removed
or updated afterwards.

For more information, see the following resources:

Component Tag Helper in ASP.NET Core


Prerender and integrate ASP.NET Core Razor components
ASP.NET Core Razor component generic
type support
Article • 05/09/2023

This article describes generic type support in Razor components.

Generic type parameter support


The @typeparam directive declares a generic type parameter for the generated
component class:

razor

@typeparam TItem

C# syntax with where type constraints is supported:

razor

@typeparam TEntity where TEntity : IEntity

In the following example, the ListGenericTypeItems1 component is generically typed as


TExample .

Shared/ListGenericTypeItems1.razor :

razor

@typeparam TExample

@if (ExampleList is not null)


{
<ul>
@foreach (var item in ExampleList)
{
<li>@item</li>
}
</ul>
}

@code {
[Parameter]
public IEnumerable<TExample>? ExampleList{ get; set; }
}
The following GenericTypeExample1 component renders two ListGenericTypeItems1
components:

String or integer data is assigned to the ExampleList parameter of each


component.
Type string or int that matches the type of the assigned data is set for the type
parameter ( TExample ) of each component.

Pages/GenericTypeExample1.razor :

razor

@page "/generic-type-example-1"

<h1>Generic Type Example 1</h1>

<ListGenericTypeItems1 ExampleList="@(new List<string> { "Item 1", "Item 2"


})"
TExample="string" />

<ListGenericTypeItems1 ExampleList="@(new List<int> { 1, 2, 3 })"


TExample="int" />

For more information, see Razor syntax reference for ASP.NET Core. For an example of
generic typing with templated components, see ASP.NET Core Blazor templated
components.

Cascaded generic type support


An ancestor component can cascade a type parameter by name to descendants using
the [CascadingTypeParameter] attribute. This attribute allows a generic type inference to
use the specified type parameter automatically with descendants that have a type
parameter with the same name.

By adding @attribute [CascadingTypeParameter(...)] to a component, the specified


generic type argument is automatically used by descendants that:

Are nested as child content for the component in the same .razor document.
Also declare a @typeparam with the exact same name.
Don't have another value explicitly supplied or implicitly inferred for the type
parameter. If another value is supplied or inferred, it takes precedence over the
cascaded generic type.
When receiving a cascaded type parameter, components obtain the parameter value
from the closest ancestor that has a CascadingTypeParameterAttribute with a matching
name. Cascaded generic type parameters are overridden within a particular subtree.

Matching is only performed by name. Therefore, we recommend avoiding a cascaded


generic type parameter with a generic name, for example T or TItem . If a developer
opts into cascading a type parameter, they're implicitly promising that its name is
unique enough not to clash with other cascaded type parameters from unrelated
components.

Generic types can be cascaded to child components in either of the following


approaches with ancestor (parent) components, which are demonstrated in the
following two sub-sections:

Explicitly set the cascaded generic type.


Infer the cascaded generic type.

The following subsections provide examples of the preceding approaches using the
following two ListDisplay components. The components receive and render list data
and are generically typed as TExample . These components are for demonstration
purposes and only differ in the color of text that the list is rendered. If you wish to
experiment with the components in the following sub-sections in a local test app, add
the following two components to the app first.

Shared/ListDisplay1.razor :

razor

@typeparam TExample

@if (ExampleList is not null)


{
<ul style="color:blue">
@foreach (var item in ExampleList)
{
<li>@item</li>
}
</ul>
}

@code {
[Parameter]
public IEnumerable<TExample>? ExampleList { get; set; }
}

Shared/ListDisplay2.razor :
razor

@typeparam TExample

@if (ExampleList is not null)


{
<ul style="color:red">
@foreach (var item in ExampleList)
{
<li>@item</li>
}
</ul>
}

@code {
[Parameter]
public IEnumerable<TExample>? ExampleList { get; set; }
}

Explicit generic types based on ancestor components


The demonstration in this section cascades a type explicitly for TExample .

7 Note

This section uses the two ListDisplay components in the Cascaded generic type
support section.

The following ListGenericTypeItems2 component receives data and cascades a generic


type parameter named TExample to its descendent components. In the upcoming parent
component, the ListGenericTypeItems2 component is used to display list data with the
preceding ListDisplay component.

Shared/ListGenericTypeItems2.razor :

razor

@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Generic Type Items 2</h2>

@ChildContent

@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}

The following GenericTypeExample2 parent component sets the child content


(RenderFragment) of two ListGenericTypeItems2 components specifying the
ListGenericTypeItems2 types ( TExample ), which are cascaded to child components.

ListDisplay components are rendered with the list item data shown in the example.
String data is used with the first ListGenericTypeItems2 component, and integer data is
used with the second ListGenericTypeItems2 component.

Pages/GenericTypeExample2.razor :

razor

@page "/generic-type-example-2"

<h1>Generic Type Example 2</h1>

<ListGenericTypeItems2 TExample="string">
<ListDisplay1 ExampleList="@(new List<string> { "Item 1", "Item 2" })"
/>
<ListDisplay2 ExampleList="@(new List<string> { "Item 3", "Item 4" })"
/>
</ListGenericTypeItems2>

<ListGenericTypeItems2 TExample="int">
<ListDisplay1 ExampleList="@(new List<int> { 1, 2, 3 })" />
<ListDisplay2 ExampleList="@(new List<int> { 4, 5, 6 })" />
</ListGenericTypeItems2>

Specifying the type explicitly also allows the use of cascading values and parameters to
provide data to child components, as the following demonstration shows.

Shared/ListDisplay3.razor :

razor

@typeparam TExample

@if (ExampleList is not null)


{
<ul style="color:blue">
@foreach (var item in ExampleList)
{
<li>@item</li>
}
</ul>
}
@code {
[CascadingParameter]
protected IEnumerable<TExample>? ExampleList { get; set; }
}

Shared/ListDisplay4.razor :

razor

@typeparam TExample

@if (ExampleList is not null)


{
<ul style="color:red">
@foreach (var item in ExampleList)
{
<li>@item</li>
}
</ul>
}

@code {
[CascadingParameter]
protected IEnumerable<TExample>? ExampleList { get; set; }
}

Shared/ListGenericTypeItems3.razor :

razor

@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Generic Type Items 3</h2>

@ChildContent

@if (ExampleList is not null)


{
<ul style="color:green">
@foreach(var item in ExampleList)
{
<li>@item</li>
}
</ul>

<p>
Type of <code>TExample</code>: @typeof(TExample)
</p>
}
@code {
[CascadingParameter]
protected IEnumerable<TExample>? ExampleList { get; set; }

[Parameter]
public RenderFragment? ChildContent { get; set; }
}

When cascading the data in the following example, the type must be provided to the
ListGenericTypeItems3 component.

Pages/GenericTypeExample3.razor :

razor

@page "/generic-type-example-3"

<h1>Generic Type Example 3</h1>

<CascadingValue Value="@stringData">
<ListGenericTypeItems3 TExample="string">
<ListDisplay3 />
<ListDisplay4 />
</ListGenericTypeItems3>
</CascadingValue>

<CascadingValue Value="@integerData">
<ListGenericTypeItems3 TExample="int">
<ListDisplay3 />
<ListDisplay4 />
</ListGenericTypeItems3>
</CascadingValue>

@code {
private List<string> stringData = new() { "Item 1", "Item 2" };
private List<int> integerData = new() { 1, 2, 3 };
}

When multiple generic types are cascaded, values for all generic types in the set must be
passed. In the following example, TItem , TValue , and TEdit are GridColumn generic
types, but the parent component that places GridColumn doesn't specify the TItem type:

razor

<GridColumn TValue="string" TEdit="@TextEdit" />


The preceding example generates a compile-time error that the GridColumn component
is missing the TItem type parameter. Valid code specifies all of the types:

razor

<GridColumn TValue="string" TEdit="@TextEdit" TItem="@User" />

Infer generic types based on ancestor components


The demonstration in this section cascades a type inferred for TExample .

7 Note

This section uses the two ListDisplay components in the Cascaded generic type
support section.

Shared/ListGenericTypeItems4.razor :

razor

@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Generic Type Items 4</h2>

@ChildContent

@if (ExampleList is not null)


{
<ul style="color:green">
@foreach(var item in ExampleList)
{
<li>@item</li>
}
</ul>

<p>
Type of <code>TExample</code>: @typeof(TExample)
</p>
}

@code {
[Parameter]
public IEnumerable<TExample>? ExampleList { get; set; }

[Parameter]
public RenderFragment? ChildContent { get; set; }
}
The following GenericTypeExample4 component with inferred cascaded types provides
different data for display.

Pages/GenericTypeExample4.razor :

razor

@page "/generic-type-example-4"

<h1>Generic Type Example 4</h1>

<ListGenericTypeItems4 ExampleList="@(new List<string> { "Item 5", "Item 6"


})">
<ListDisplay1 ExampleList="@(new List<string> { "Item 1", "Item 2" })"
/>
<ListDisplay2 ExampleList="@(new List<string> { "Item 3", "Item 4" })"
/>
</ListGenericTypeItems4>

<ListGenericTypeItems4 ExampleList="@(new List<int> { 7, 8, 9 })">


<ListDisplay1 ExampleList="@(new List<int> { 1, 2, 3 })" />
<ListDisplay2 ExampleList="@(new List<int> { 4, 5, 6 })" />
</ListGenericTypeItems4>

The following GenericTypeExample5 component with inferred cascaded types provides


the same data for display. The following example directly assigns the data to the
components.

Pages/GenericTypeExample5.razor :

razor

@page "/generic-type-example-5"

<h1>Generic Type Example 5</h1>

<ListGenericTypeItems4 ExampleList="@stringData">
<ListDisplay1 ExampleList="@stringData" />
<ListDisplay2 ExampleList="@stringData" />
</ListGenericTypeItems4>

<ListGenericTypeItems4 ExampleList="@integerData">
<ListDisplay1 ExampleList="@integerData" />
<ListDisplay2 ExampleList="@integerData" />
</ListGenericTypeItems4>

@code {
private List<string> stringData = new() { "Item 1", "Item 2" };
private List<int> integerData = new() { 1, 2, 3 };
}
ASP.NET Core Blazor synchronization
context
Article • 05/15/2023

Blazor uses a synchronization context (SynchronizationContext) to enforce a single


logical thread of execution. A component's lifecycle methods and event callbacks raised
by Blazor are executed on the synchronization context.

Blazor Server's synchronization context attempts to emulate a single-threaded


environment so that it closely matches the WebAssembly model in the browser, which is
single threaded. At any given point in time, work is performed on exactly one thread,
which yields the impression of a single logical thread. No two operations execute
concurrently.

Avoid thread-blocking calls


Generally, don't call the following methods in components. The following methods
block the execution thread and thus block the app from resuming work until the
underlying Task is complete:

Result
Wait
WaitAny
WaitAll
Sleep
GetResult

7 Note

Blazor documentation examples that use the thread-blocking methods mentioned


in this section are only using the methods for demonstration purposes, not as
recommended coding guidance. For example, a few component code
demonstrations simulate a long-running process by calling Thread.Sleep.

Invoke component methods externally to


update state
In the event a component must be updated based on an external event, such as a timer
or other notification, use the InvokeAsync method, which dispatches code execution to
Blazor's synchronization context. For example, consider the following notifier service that
can notify any listening component about updated state. The Update method can be
called from anywhere in the app.

TimerService.cs :

C#

public class TimerService : IDisposable


{
private int elapsedCount;
private readonly static TimeSpan heartbeatTickRate =
TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private PeriodicTimer? timer;

public TimerService(NotifierService notifier,


ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}

public async Task Start()


{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");

using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation($"elapsedCount: {elapsedCount}");
}
}
}
}

public void Dispose()


{
timer?.Dispose();
}
}
NotifierService.cs :

C#

public class NotifierService


{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}

public event Func<string, int, Task>? Notify;


}

Register the services:

In a Blazor WebAssembly app, register the services as singletons in Program.cs :

C#

builder.Services.AddSingleton<NotifierService>();
builder.Services.AddSingleton<TimerService>();

In a Blazor Server app, register the services as scoped in Program.cs :

C#

builder.Services.AddScoped<NotifierService>();
builder.Services.AddScoped<TimerService>();

Use the NotifierService to update a component.

Pages/ReceiveNotifications.razor :

razor

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting first notification</span>
}
</p>

@code {
private (string key, int value) lastNotification;

protected override void OnInitialized()


{
Notifier.Notify += OnNotify;
}

public async Task OnNotify(string key, int value)


{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}

private async Task StartTimer()


{
await Timer.Start();
}

public void Dispose()


{
Notifier.Notify -= OnNotify;
}
}

In the preceding example:

NotifierService invokes the component's OnNotify method outside of Blazor's

synchronization context. InvokeAsync is used to switch to the correct context and


queue a render. For more information, see ASP.NET Core Razor component
rendering.
The component implements IDisposable. The OnNotify delegate is unsubscribed in
the Dispose method, which is called by the framework when the component is
disposed. For more information, see ASP.NET Core Razor component lifecycle.

) Important

If a Razor component defines an event that's triggered from a background thread,


the component might be required to capture and restore the execution context
(ExecutionContext) at the time the handler is registered. For more information, see
Calling InvokeAsync(StateHasChanged) causes page to fallback to default culture
(dotnet/aspnetcore #28521) .
Retain element, component, and model
relationships in ASP.NET Core Blazor
Article • 05/24/2023

When rendering a list of elements or components and the elements or components


subsequently change, Blazor must decide which of the previous elements or
components are retained and how model objects should map to them. Normally, this
process is automatic and sufficient for general rendering, but there are often cases
where controlling the process using the @key directive attribute is required.

Consider the following example that demonstrates a collection mapping problem that's
solved by using @key.

For the following Details and PeopleExample components:

The Details component receives data ( Data ) from the parent PeopleExample
component, which is displayed in an <input> element. Any given displayed
<input> element can receive the focus of the page from the user when they select
one of the <input> elements.
The PeopleExample component creates a list of person objects for display using the
Details component. Every three seconds, a new person is added to the collection.

This demonstration allows you to:

Select an <input> from among several rendered Details components.


Study the behavior of the page's focus as the people collection automatically
grows.

Shared/Details.razor :

razor

<input value="@Data" />

@code {
[Parameter]
public string? Data { get; set; }
}

In the following PeopleExample component, each iteration of adding a person in


OnTimerCallback results in Blazor rebuilding the entire collection. The page's focus
remains on the same index position of <input> elements, so the focus shifts each time a
person is added. Shifting the focus away from what the user selected isn't desirable
behavior. After demonstrating the poor behavior with the following component, the
@key directive attribute is used to improve the user's experience.

Pages/PeopleExample.razor :

razor

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)


{
<Details Data="@person.Data" />
}

@code {
private Timer timer = new Timer(3000);

public List<Person> people =


new()
{
{ new Person { Data = "Person 1" } },
{ new Person { Data = "Person 2" } },
{ new Person { Data = "Person 3" } }
};

protected override void OnInitialized()


{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}

private void OnTimerCallback()


{
_ = InvokeAsync(() =>
{
people.Insert(0,
new Person
{
Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss
tt")}"
});
StateHasChanged();
});
}

public void Dispose() => timer.Dispose();

public class Person


{
public string? Data { get; set; }
}
}

The contents of the people collection changes with inserted, deleted, or re-ordered
entries. Rerendering can lead to visible behavior differences. For example, each time a
person is inserted into the people collection, the user's focus is lost.

The mapping process of elements or components to a collection can be controlled with


the @key directive attribute. Use of @key guarantees the preservation of elements or
components based on the key's value. If the Details component in the preceding
example is keyed on the person item, Blazor ignores rerendering Details components
that haven't changed.

To modify the PeopleExample component to use the @key directive attribute with the
people collection, update the <Details> element to the following:

razor

<Details @key="person" Data="@person.Data" />

When the people collection changes, the association between Details instances and
person instances is retained. When a Person is inserted at the beginning of the

collection, one new Details instance is inserted at that corresponding position. Other
instances are left unchanged. Therefore, the user's focus isn't lost as people are added
to the collection.

Other collection updates exhibit the same behavior when the @key directive attribute is
used:

If an instance is deleted from the collection, only the corresponding component


instance is removed from the UI. Other instances are left unchanged.
If collection entries are re-ordered, the corresponding component instances are
preserved and re-ordered in the UI.

) Important

Keys are local to each container element or component. Keys aren't compared
globally across the document.

When to use @key


Typically, it makes sense to use @key whenever a list is rendered (for example, in a
foreach block) and a suitable value exists to define the @key.

You can also use @key to preserve an element or component subtree when an object
doesn't change, as the following examples show.

Example 1:

razor

<li @key="person">
<input value="@person.Data" />
</li>

Example 2:

razor

<div @key="person">
@* other HTML elements *@
</div>

If an person instance changes, the @key attribute directive forces Blazor to:

Discard the entire <li> or <div> and their descendants.


Rebuild the subtree within the UI with new elements and components.

This is useful to guarantee that no UI state is preserved when the collection changes
within a subtree.

Scope of @key
The @key attribute directive is scoped to its own siblings within its parent.

Consider the following example. The first and second keys are compared against each
other within the same scope of the outer <div> element:

razor

<div>
<div @key="first">...</div>
<div @key="second">...</div>
</div>
The following example demonstrates first and second keys in their own scopes,
unrelated to each other and without influence on each other. Each @key scope only
applies to its parent <div> element, not across the parent <div> elements:

razor

<div>
<div @key="first">...</div>
</div>
<div>
<div @key="second">...</div>
</div>

For the Details component shown earlier, the following examples render person data
within the same @key scope and demonstrate typical use cases for @key:

razor

<div>
@foreach (var person in people)
{
<Details @key="person" Data="@person.Data" />
}
</div>

razor

@foreach (var person in people)


{
<div @key="person">
<Details Data="@person.Data" />
</div>
}

razor

<ol>
@foreach (var person in people)
{
<li @key="person">
<Details Data="@person.Data" />
</li>
}
</ol>

The following examples only scope @key to the <div> or <li> element that surrounds
each Details component instance. Therefore, person data for each member of the
people collection is not keyed on each person instance across the rendered Details

components. Avoid the following patterns when using @key:

razor

@foreach (var person in people)


{
<div>
<Details @key="person" Data="@person.Data" />
</div>
}

razor

<ol>
@foreach (var person in people)
{
<li>
<Details @key="person" Data="@person.Data" />
</li>
}
</ol>

When not to use @key


There's a performance cost when rendering with @key. The performance cost isn't large,
but only specify @key if preserving the element or component benefits the app.

Even if @key isn't used, Blazor preserves child element and component instances as
much as possible. The only advantage to using @key is control over how model
instances are mapped to the preserved component instances, instead of Blazor selecting
the mapping.

Values to use for @key


Generally, it makes sense to supply one of the following values for @key:

Model object instances. For example, the Person instance ( person ) was used in the
earlier example. This ensures preservation based on object reference equality.
Unique identifiers. For example, unique identifiers can be based on primary key
values of type int , string , or Guid .

Ensure that values used for @key don't clash. If clashing values are detected within the
same parent element, Blazor throws an exception because it can't deterministically map
old elements or components to new elements or components. Only use distinct values,
such as object instances or primary key values.
Avoid overwriting parameters in
ASP.NET Core Blazor
Article • 05/15/2023

The Blazor framework generally imposes safe parent-to-child parameter assignment:

Parameters aren't overwritten unexpectedly.


Side effects are minimized. For example, additional renders are avoided because
they may create infinite rendering loops.

A child component receives new parameter values that possibly overwrite existing
values when the parent component rerenders. Accidentally overwriting parameter values
in a child component often occurs when developing the component with one or more
data-bound parameters and the developer writes directly to a parameter in the child:

The child component is rendered with one or more parameter values from the
parent component.
The child writes directly to the value of a parameter.
The parent component rerenders and overwrites the value of the child's parameter.

The potential for overwriting parameter values extends into the child component's
property set accessors, too.

) Important

Our general guidance is not to create components that directly write to their own
parameters after the component is rendered for the first time.

Consider the following Expander component that:

Renders child content.


Toggles showing child content with a component parameter ( Expanded ).

After the following Expander component demonstrates an overwritten parameter, a


modified Expander component is shown to demonstrate the correct approach for this
scenario. The following examples can be placed in a local sample app to experience the
behaviors described.

Shared/Expander.razor :

razor
<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
<div class="card-body">
<h2 class="card-title">Toggle (<code>Expanded</code> = @Expanded)
</h2>

@if (Expanded)
{
<p class="card-text">@ChildContent</p>
}
</div>
</div>

@code {
[Parameter]
public bool Expanded { get; set; }

[Parameter]
public RenderFragment? ChildContent { get; set; }

private void Toggle()


{
Expanded = !Expanded;
}
}

The Expander component is added to the following ExpanderExample parent component


that may call StateHasChanged:

Calling StateHasChanged in developer code notifies a component that its state has
changed and typically triggers component rerendering to update the UI.
StateHasChanged is covered in more detail later in ASP.NET Core Razor
component lifecycle and ASP.NET Core Razor component rendering.
The button's @onclick directive attribute attaches an event handler to the button's
onclick event. Event handling is covered in more detail later in ASP.NET Core

Blazor event handling.

Pages/ExpanderExample.razor :

razor

@page "/expander-example"

<Expander Expanded="true">
Expander 1 content
</Expander>

<Expander Expanded="true" />

<button @onclick="StateHasChanged">
Call StateHasChanged
</button>

Initially, the Expander components behave independently when their Expanded


properties are toggled. The child components maintain their states as expected.

If StateHasChanged is called in a parent component, the Blazor framework rerenders


child components if their parameters might have changed:

For a group of parameter types that Blazor explicitly checks, Blazor rerenders a
child component if it detects that any of the parameters have changed.
For unchecked parameter types, Blazor rerenders the child component regardless
of whether or not the parameters have changed. Child content falls into this
category of parameter types because child content is of type RenderFragment,
which is a delegate that refers to other mutable objects.

For the ExpanderExample component:

The first Expander component sets child content in a potentially mutable


RenderFragment, so a call to StateHasChanged in the parent component
automatically rerenders the component and potentially overwrites the value of
Expanded to its initial value of true .

The second Expander component doesn't set child content. Therefore, a potentially
mutable RenderFragment doesn't exist. A call to StateHasChanged in the parent
component doesn't automatically rerender the child component, so the
component's Expanded value isn't overwritten.

To maintain state in the preceding scenario, use a private field in the Expander
component to maintain its toggled state.

The following revised Expander component:

Accepts the Expanded component parameter value from the parent.


Assigns the component parameter value to a private field ( expanded ) in the
OnInitialized event.
Uses the private field to maintain its internal toggle state, which demonstrates how
to avoid writing directly to a parameter.

7 Note

The advice in this section extends to similar logic in component parameter set
accessors, which can result in similar undesirable side effects.
Shared/Expander.razor :

razor

<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">


<div class="card-body">
<h2 class="card-title">Toggle (<code>expanded</code> = @expanded)
</h2>

@if (expanded)
{
<p class="card-text">@ChildContent</p>
}
</div>
</div>

@code {
private bool expanded;

[Parameter]
public bool Expanded { get; set; }

[Parameter]
public RenderFragment? ChildContent { get; set; }

protected override void OnInitialized()


{
expanded = Expanded;
}

private void Toggle()


{
expanded = !expanded;
}
}

For two-way parent-child binding examples, see ASP.NET Core Blazor data binding. For
additional information, see Blazor Two Way Binding Error (dotnet/aspnetcore #24599) .

For more information on change detection, including information on the exact types
that Blazor checks, see ASP.NET Core Razor component rendering.
ASP.NET Core Blazor attribute splatting
and arbitrary parameters
Article • 05/24/2023

Components can capture and render additional attributes in addition to the


component's declared parameters. Additional attributes can be captured in a dictionary
and then splatted onto an element when the component is rendered using the
@attributes Razor directive attribute. This scenario is useful for defining a component
that produces a markup element that supports a variety of customizations. For example,
it can be tedious to define attributes separately for an <input> that supports many
parameters.

Attribute splatting
In the following Splat component:

The first <input> element ( id="useIndividualParams" ) uses individual component


parameters.
The second <input> element ( id="useAttributesDict" ) uses attribute splatting.

Pages/Splat.razor :

razor

@page "/splat"

<input id="useIndividualParams"
maxlength="@maxlength"
placeholder="@placeholder"
required="@required"
size="@size" />

<input id="useAttributesDict"
@attributes="InputAttributes" />

@code {
private string maxlength = "10";
private string placeholder = "Input placeholder text";
private string required = "required";
private string size = "50";

private Dictionary<string, object> InputAttributes { get; set; } =


new()
{
{ "maxlength", "10" },
{ "placeholder", "Input placeholder text" },
{ "required", "required" },
{ "size", "50" }
};
}

The rendered <input> elements in the webpage are identical:

HTML

<input id="useIndividualParams"
maxlength="10"
placeholder="Input placeholder text"
required="required"
size="50">

<input id="useAttributesDict"
maxlength="10"
placeholder="Input placeholder text"
required="required"
size="50">

Arbitrary attributes
To accept arbitrary attributes, define a component parameter with the
CaptureUnmatchedValues property set to true :

razor

@code {
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? InputAttributes { get; set; }
}

The CaptureUnmatchedValues property on [Parameter] allows the parameter to match


all attributes that don't match any other parameter. A component can only define a
single parameter with CaptureUnmatchedValues. The property type used with
CaptureUnmatchedValues must be assignable from Dictionary<string, object> with
string keys. Use of IEnumerable<KeyValuePair<string, object>> or
IReadOnlyDictionary<string, object> are also options in this scenario.

The position of @attributes relative to the position of element attributes is important.


When @attributes are splatted on the element, the attributes are processed from right
to left (last to first). Consider the following example of a parent component that
consumes a child component:
Shared/AttributeOrderChild1.razor :

razor

<div @attributes="AdditionalAttributes" extra="5" />

@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }
}

Pages/AttributeOrderParent1.razor :

razor

@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />

The AttributeOrderChild1 component's extra attribute is set to the right of


@attributes. The AttributeOrderParent1 component's rendered <div> contains
extra="5" when passed through the additional attribute because the attributes are

processed right to left (last to first):

HTML

<div extra="5" />

In the following example, the order of extra and @attributes is reversed in the child
component's <div> :

Shared/AttributeOrderChild2.razor :

razor

<div extra="5" @attributes="AdditionalAttributes" />

@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }
}

Pages/AttributeOrderParent2.razor :

razor
@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />

The <div> in the parent component's rendered webpage contains extra="10" when
passed through the additional attribute:

HTML

<div extra="10" />


ASP.NET Core Blazor layouts
Article • 03/13/2023

This article explains how to create reusable layout components for Blazor apps.

Some app elements, such as menus, copyright messages, and company logos, are
usually part of app's overall presentation. Placing a copy of the markup for these
elements into all of the components of an app isn't efficient. Every time that one of
these elements is updated, every component that uses the element must be updated.
This approach is costly to maintain and can lead to inconsistent content if an update is
missed. Layouts solve these problems.

A Blazor layout is a Razor component that shares markup with components that
reference it. Layouts can use data binding, dependency injection, and other features of
components.

Layout components

Create a layout component


To create a layout component:

Create a Razor component defined by a Razor template or C# code. Layout


components based on a Razor template use the .razor file extension just like
ordinary Razor components. Because layout components are shared across an
app's components, they're usually placed in the app's Shared folder. However,
layouts can be placed in any location accessible to the components that use it. For
example, a layout can be placed in the same folder as the components that use it.
Inherit the component from LayoutComponentBase. The LayoutComponentBase
defines a Body property (RenderFragment type) for the rendered content inside
the layout.
Use the Razor syntax @Body to specify the location in the layout markup where the
content is rendered.

7 Note

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


The following DoctorWhoLayout component shows the Razor template of a layout
component. The layout inherits LayoutComponentBase and sets the @Body between the
navigation bar ( <nav>...</nav> ) and the footer ( <footer>...</footer> ).

Shared/DoctorWhoLayout.razor :

razor

@inherits LayoutComponentBase

<header>
<h1>Doctor Who™ Episode Database</h1>
</header>

<nav>
<a href="main-list">Main Episode List</a>
<a href="search">Search</a>
<a href="new">Add Episode</a>
</nav>

@Body

<footer>
@TrademarkMessage
</footer>

@code {
public string TrademarkMessage { get; set; } =
"Doctor Who is a registered trademark of the BBC. " +
"https://www.doctorwho.tv/";
}

MainLayout component

In an app created from a Blazor project template, the MainLayout component is the
app's default layout. Blazor's layout adopts the Flexbox layout model (MDN
documentation) (W3C specification ).

Shared/MainLayout.razor :

razor

@inherits LayoutComponentBase

<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-
auto">About</a>
</div>

<div class="content px-4">


@Body
</div>
</main>
</div>

Blazor's CSS isolation feature applies isolated CSS styles to the MainLayout component.
By convention, the styles are provided by the accompanying stylesheet of the same
name, Shared/MainLayout.razor.css . The ASP.NET Core framework implementation of
the stylesheet is available for inspection in the ASP.NET Core reference source
( dotnet/aspnetcore GitHub repository):

Blazor Server MainLayout.razor.css


Blazor WebAssembly MainLayout.razor.css

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Apply a layout

Apply a layout to a component


Use the @layout Razor directive to apply a layout to a routable Razor component that
has an @page directive. The compiler converts @layout into a LayoutAttribute and
applies the attribute to the component class.

The content of the following Episodes component is inserted into the DoctorWhoLayout
at the position of @Body .

Pages/Episodes.razor :
razor

@page "/episodes"
@layout DoctorWhoLayout

<h2>Episodes</h2>

<ul>
<li>
<a href="https://www.bbc.co.uk/programmes/p00vfknq">
<em>The Ribos Operation</em>
</a>
</li>
<li>
<a href="https://www.bbc.co.uk/programmes/p00vfdsb">
<em>The Sun Makers</em>
</a>
</li>
<li>
<a href="https://www.bbc.co.uk/programmes/p00vhc26">
<em>Nightmare of Eden</em>
</a>
</li>
</ul>

The following rendered HTML markup is produced by the preceding DoctorWhoLayout


and Episodes component. Extraneous markup doesn't appear in order to focus on the
content provided by the two components involved:

The Doctor Who™ Episode Database heading ( <h1>...</h1> ) in the header


( <header>...</header> ), navigation bar ( <nav>...</nav> ), and trademark
information element ( <div>...</div> ) in the footer ( <footer>...</footer> ) come
from the DoctorWhoLayout component.
The Episodes heading ( <h2>...</h2> ) and episode list ( <ul>...</ul> ) come from
the Episodes component.

HTML

<body>
<div id="app">
<header>
<h1>Doctor Who&trade; Episode Database</h1>
</header>

<nav>
<a href="main-list">Main Episode List</a>
<a href="search">Search</a>
<a href="new">Add Episode</a>
</nav>
<h2>Episodes</h2>

<ul>
<li>...</li>
<li>...</li>
<li>...</li>
</ul>

<footer>
Doctor Who is a registered trademark of the BBC.
https://www.doctorwho.tv/
</footer>
</div>
</body>

Specifying the layout directly in a component overrides a default layout:

Set by an @layout directive imported from an _Imports component


( _Imports.razor ), as described in the following Apply a layout to a folder of
components section.
Set as the app's default layout, as described in the Apply a default layout to an app
section later in this article.

Apply a layout to a folder of components


Every folder of an app can optionally contain a template file named _Imports.razor . The
compiler includes the directives specified in the imports file in all of the Razor templates
in the same folder and recursively in all of its subfolders. Therefore, an _Imports.razor
file containing @layout DoctorWhoLayout ensures that all of the components in a folder
use the DoctorWhoLayout component. There's no need to repeatedly add @layout
DoctorWhoLayout to all of the Razor components ( .razor ) within the folder and

subfolders.

_Imports.razor :

razor

@layout DoctorWhoLayout
...

The _Imports.razor file is similar to the _ViewImports.cshtml file for Razor views and
pages but applied specifically to Razor component files.
Specifying a layout in _Imports.razor overrides a layout specified as the router's default
app layout, which is described in the following section.

2 Warning

Do not add a Razor @layout directive to the root _Imports.razor file, which results
in an infinite loop of layouts. To control the default app layout, specify the layout in
the Router component. For more information, see the following Apply a default
layout to an app section.

7 Note

The @layout Razor directive only applies a layout to routable Razor components
with an @page directive.

Apply a default layout to an app


Specify the default app layout in the App component's Router component. The following
example from an app based on a Blazor project template sets the default layout to the
MainLayout component.

App.razor :

razor

<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>

For more information on the Router component, see ASP.NET Core Blazor routing and
navigation.

Specifying the layout as a default layout in the Router component is a useful practice
because you can override the layout on a per-component or per-folder basis, as
described in the preceding sections of this article. We recommend using the Router
component to set the app's default layout because it's the most general and flexible
approach for using layouts.

Apply a layout to arbitrary content ( LayoutView


component)
To set a layout for arbitrary Razor template content, specify the layout with a LayoutView
component. You can use a LayoutView in any Razor component. The following example
sets a layout component named ErrorLayout for the MainLayout component's
NotFound template ( <NotFound>...</NotFound> ).

App.razor :

razor

<Router ...>
<Found ...>
...
</Found>
<NotFound>
<LayoutView Layout="@typeof(ErrorLayout)">
<h1>Page not found</h1>
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

Nested layouts
A component can reference a layout that in turn references another layout. For example,
nested layouts are used to create a multi-level menu structures.

The following example shows how to use nested layouts. The Episodes component
shown in the Apply a layout to a component section is the component to display. The
component references the DoctorWhoLayout component.

The following DoctorWhoLayout component is a modified version of the example shown


earlier in this article. The header and footer elements are removed, and the layout
references another layout, ProductionsLayout . The Episodes component is rendered
where @Body appears in the DoctorWhoLayout .

Shared/DoctorWhoLayout.razor :
razor

@inherits LayoutComponentBase
@layout ProductionsLayout

<h1>Doctor Who™ Episode Database</h1>

<nav>
<a href="main-episode-list">Main Episode List</a>
<a href="episode-search">Search</a>
<a href="new-episode">Add Episode</a>
</nav>

@Body

<div>
@TrademarkMessage
</div>

@code {
public string TrademarkMessage { get; set; } =
"Doctor Who is a registered trademark of the BBC. " +
"https://www.doctorwho.tv/";
}

The ProductionsLayout component contains the top-level layout elements, where the
header ( <header>...</header> ) and footer ( <footer>...</footer> ) elements now reside.
The DoctorWhoLayout with the Episodes component is rendered where @Body appears.

Shared/ProductionsLayout.razor :

razor

@inherits LayoutComponentBase

<header>
<h1>Productions</h1>
</header>

<nav>
<a href="main-production-list">Main Production List</a>
<a href="production-search">Search</a>
<a href="new-production">Add Production</a>
</nav>

@Body

<footer>
Footer of Productions Layout
</footer>
The following rendered HTML markup is produced by the preceding nested layout.
Extraneous markup doesn't appear in order to focus on the nested content provided by
the three components involved:

The header ( <header>...</header> ), production navigation bar ( <nav>...</nav> ),


and footer ( <footer>...</footer> ) elements and their content come from the
ProductionsLayout component.

The Doctor Who™ Episode Database heading ( <h1>...</h1> ), episode navigation


bar ( <nav>...</nav> ), and trademark information element ( <div>...</div> ) come
from the DoctorWhoLayout component.
The Episodes heading ( <h2>...</h2> ) and episode list ( <ul>...</ul> ) come from
the Episodes component.

HTML

<body>
<div id="app">
<header>
<h1>Productions</h1>
</header>

<nav>
<a href="main-production-list">Main Production List</a>
<a href="production-search">Search</a>
<a href="new-production">Add Production</a>
</nav>

<h1>Doctor Who&trade; Episode Database</h1>

<nav>
<a href="main-episode-list">Main Episode List</a>
<a href="episode-search">Search</a>
<a href="new-episode">Add Episode</a>
</nav>

<h2>Episodes</h2>

<ul>
<li>...</li>
<li>...</li>
<li>...</li>
</ul>

<div>
Doctor Who is a registered trademark of the BBC.
https://www.doctorwho.tv/
</div>

<footer>
Footer of Productions Layout
</footer>
</div>
</body>

Share a Razor Pages layout with integrated


components
When routable components are integrated into a Razor Pages app, the app's shared
layout can be used with the components. For more information, see Prerender and
integrate ASP.NET Core Razor components.

Additional resources
Layout in ASP.NET Core
Blazor samples GitHub repository (dotnet/blazor-samples)
Control <head> content in ASP.NET Core
Blazor apps
Article • 04/04/2023

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).

Control <head> content in a Razor component


Specify the page's title with the PageTitle component, which enables rendering an HTML
<title> element to a HeadOutlet component.

Specify <head> element content with the HeadContent component, which provides
content to a HeadOutlet component.

The following example sets the page's title and description using Razor.

Pages/ControlHeadContent.razor :

razor

@page "/control-head-content"

<h1>Control <head> content</h1>

<p>
Title: @title
</p>

<p>
Description: @description
</p>

<PageTitle>@title</PageTitle>

<HeadContent>
<meta name="description" content="@description">
</HeadContent>

@code {
private string description = "Description set by component";
private string title = "Title set by component";
}
HeadOutlet component
The HeadOutlet component renders content provided by PageTitle and HeadContent
components.

In an app created from the Blazor WebAssembly project template, the HeadOutlet
component is added to the RootComponents collection of the
WebAssemblyHostBuilder in Program.cs :

C#

builder.RootComponents.Add<HeadOutlet>("head::after");

When the ::after pseudo-selector is specified, the contents of the root component are
appended to the existing head contents instead of replacing the content. This allows the
app to retain static head content in wwwroot/index.html without having to repeat the
content in the app's Razor components.

In Blazor Server apps created from the Blazor Server project template, a Component Tag
Helper renders <head> content for the HeadOutlet component in Pages/_Host.cshtml :

CSHTML

<head>
...
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>

Not found page title


In Blazor apps created from Blazor project templates, the NotFound component template
in the App component ( App.razor ) sets the page title to Not found .

App.razor :

razor

<PageTitle>Not found</PageTitle>

Additional resources
Control headers in C# code at startup
Blazor samples GitHub repository (dotnet/blazor-samples)

Mozilla MDN Web Docs documentation:

What's in the head? Metadata in HTML


<head>: The Document Metadata (Header) element
<title>: The Document Title element
<meta>: The metadata element
ASP.NET Core Blazor cascading values
and parameters
Article • 04/27/2023

This article explains how to flow data from an ancestor Razor component to descendent
components.

Cascading values and parameters provide a convenient way to flow data down a
component hierarchy from an ancestor component to any number of descendent
components. Unlike Component parameters, cascading values and parameters don't
require an attribute assignment for each descendent component where the data is
consumed. Cascading values and parameters also allow components to coordinate with
each other across a component hierarchy.

7 Note

The code examples in this article adopt nullable reference types (NRTs) and .NET
compiler null-state static analysis, which are supported in ASP.NET Core 6.0 or
later. When targeting ASP.NET Core 5.0 or earlier, remove the null type designation
( ? ) from the CascadingType? , @ActiveTab? , RenderFragment? , ITab? , TabSet? , and
string? types in the article's examples.

CascadingValue component
An ancestor component provides a cascading value using the Blazor framework's
CascadingValue component, which wraps a subtree of a component hierarchy and
supplies a single value to all of the components within its subtree.

The following example demonstrates the flow of theme information down the
component hierarchy of a layout component to provide a CSS style class to buttons in
child components.

The following ThemeInfo C# class is placed in a folder named UIThemeClasses and


specifies the theme information.

7 Note

For the examples in this section, the app's namespace is BlazorSample . When
experimenting with the code in your own sample app, change the app's namespace
to your sample app's namespace.

UIThemeClasses/ThemeInfo.cs :

C#

namespace BlazorSample.UIThemeClasses;

public class ThemeInfo


{
public string? ButtonClass { get; set; }
}

The following layout component specifies theme information ( ThemeInfo ) as a cascading


value for all components that make up the layout body of the Body property.
ButtonClass is assigned a value of btn-success , which is a Bootstrap button style. Any
descendent component in the component hierarchy can use the ButtonClass property
through the ThemeInfo cascading value.

Shared/MainLayout.razor :

razor

@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
<div class="sidebar">
<NavMenu />
</div>

<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/"
target="_blank">About</a>
</div>

<CascadingValue Value="@theme">
<article class="content px-4">
@Body
</article>
</CascadingValue>
</main>
</div>

@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
[CascadingParameter] attribute
To make use of cascading values, descendent components declare cascading parameters
using the [CascadingParameter] attribute. Cascading values are bound to cascading
parameters by type. Cascading multiple values of the same type is covered in the
Cascade multiple values section later in this article.

The following component binds the ThemeInfo cascading value to a cascading


parameter, optionally using the same name of ThemeInfo . The parameter is used to set
the CSS class for the Increment Counter (Themed) button.

Pages/ThemedCounter.razor :

razor

@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
<button @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>

<p>
<button
class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass :
string.Empty)"
@onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>

@code {
private int currentCount = 0;

[CascadingParameter]
protected ThemeInfo? ThemeInfo { get; set; }

private void IncrementCount()


{
currentCount++;
}
}
Similar to a regular component parameter, components accepting a cascading
parameter are rerendered when the cascading value is changed. For instance,
configuring a different theme instance causes the ThemedCounter component from the
CascadingValue component section to rerender:

Shared/MainLayout.razor :

razor

<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/"
target="_blank">About</a>
</div>

<CascadingValue Value="@theme">
<article class="content px-4">
@Body
</article>
</CascadingValue>
<button @onclick="ChangeToDarkTheme">Dark mode</button>
</main>

@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };

private void ChangeToDarkTheme()


{
theme = new() { ButtonClass = "btn-darkmode-success" };
}
}

CascadingValue<TValue>.IsFixed can be used to indicate that a cascading parameter


doesn't change after initialization.

Cascade multiple values


To cascade multiple values of the same type within the same subtree, provide a unique
Name string to each CascadingValue component and their corresponding
[CascadingParameter] attributes.

In the following example, two CascadingValue components cascade different instances


of CascadingType :

razor
<CascadingValue Value="@parentCascadeParameter1" Name="CascadeParam1">
<CascadingValue Value="@ParentCascadeParameter2" Name="CascadeParam2">
...
</CascadingValue>
</CascadingValue>

@code {
private CascadingType? parentCascadeParameter1;

[Parameter]
public CascadingType? ParentCascadeParameter2 { get; set; }
}

In a descendant component, the cascaded parameters receive their cascaded values


from the ancestor component by Name:

razor

@code {
[CascadingParameter(Name = "CascadeParam1")]
protected CascadingType? ChildCascadeParameter1 { get; set; }

[CascadingParameter(Name = "CascadeParam2")]
protected CascadingType? ChildCascadeParameter2 { get; set; }
}

Pass data across a component hierarchy


Cascading parameters also enable components to pass data across a component
hierarchy. Consider the following UI tab set example, where a tab set component
maintains a series of individual tabs.

7 Note

For the examples in this section, the app's namespace is BlazorSample . When
experimenting with the code in your own sample app, change the namespace to
your sample app's namespace.

Create an ITab interface that tabs implement in a folder named UIInterfaces .

UIInterfaces/ITab.cs :

C#
using Microsoft.AspNetCore.Components;

namespace BlazorSample.UIInterfaces;

public interface ITab


{
RenderFragment ChildContent { get; }
}

7 Note

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

The following TabSet component maintains a set of tabs. The tab set's Tab components,
which are created later in this section, supply the list items ( <li>...</li> ) for the list
( <ul>...</ul> ).

Child Tab components aren't explicitly passed as parameters to the TabSet . Instead, the
child Tab components are part of the child content of the TabSet . However, the TabSet
still needs a reference each Tab component so that it can render the headers and the
active tab. To enable this coordination without requiring additional code, the TabSet
component can provide itself as a cascading value that is then picked up by the
descendent Tab components.

Shared/TabSet.razor :

razor

@using BlazorSample.UIInterfaces

<!-- Display the tab headers -->

<CascadingValue Value="this">
<ul class="nav nav-tabs">
@ChildContent
</ul>
</CascadingValue>

<!-- Display body for only the active tab -->

<div class="nav-tabs-body p-4">


@ActiveTab?.ChildContent
</div>

@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }

public ITab? ActiveTab { get; private set; }

public void AddTab(ITab tab)


{
if (ActiveTab is null)
{
SetActiveTab(tab);
}
}

public void SetActiveTab(ITab tab)


{
if (ActiveTab != tab)
{
ActiveTab = tab;
StateHasChanged();
}
}
}

Descendent Tab components capture the containing TabSet as a cascading parameter.


The Tab components add themselves to the TabSet and coordinate to set the active
tab.

Shared/Tab.razor :

razor

@using BlazorSample.UIInterfaces
@implements ITab

<li>
<a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
@Title
</a>
</li>

@code {
[CascadingParameter]
public TabSet? ContainerTabSet { get; set; }

[Parameter]
public string? Title { get; set; }

[Parameter]
public RenderFragment? ChildContent { get; set; }

private string? TitleCssClass =>


ContainerTabSet?.ActiveTab == this ? "active" : null;
protected override void OnInitialized()
{
ContainerTabSet?.AddTab(this);
}

private void ActivateTab()


{
ContainerTabSet?.SetActiveTab(this);
}
}

The following ExampleTabSet component uses the TabSet component, which contains
three Tab components.

Pages/ExampleTabSet.razor :

razor

@page "/example-tab-set"

<TabSet>
<Tab Title="First tab">
<h4>Greetings from the first tab!</h4>

<label>
<input type="checkbox" @bind="showThirdTab" />
Toggle third tab
</label>
</Tab>

<Tab Title="Second tab">


<h4>Hello from the second tab!</h4>
</Tab>

@if (showThirdTab)
{
<Tab Title="Third tab">
<h4>Welcome to the disappearing third tab!</h4>
<p>Toggle this tab from the first tab.</p>
</Tab>
}
</TabSet>

@code {
private bool showThirdTab;
}
ASP.NET Core Blazor data binding
Article • 06/05/2023

This article explains data binding features for Razor components and Document Object
Model (DOM) elements in Blazor apps.

Razor components provide data binding features with the @bind Razor directive
attribute with a field, property, or Razor expression value.

The following example binds:

An <input> element value to the C# inputValue field.


A second <input> element value to the C# InputValue property.

When an <input> element loses focus, its bound field or property is updated.

Pages/Bind.razor :

razor

@page "/bind"

<p>
<input @bind="inputValue" />
</p>

<p>
<input @bind="InputValue" />
</p>

<ul>
<li><code>inputValue</code>: @inputValue</li>
<li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
private string? inputValue;

private string? InputValue { get; set; }


}

The text box is updated in the UI only when the component is rendered, not in response
to changing the field's or property's value. Since components render themselves after
event handler code executes, field and property updates are usually reflected in the UI
immediately after an event handler is triggered.
As a demonstration of how data binding composes in HTML, the following example
binds the InputValue property to the second <input> element's value and onchange
attributes (change ). The second <input> element in the following example is a concept
demonstration and isn't meant to suggest how you should bind data in Razor components.

Pages/BindTheory.razor :

razor

@page "/bind-theory"

<p>
<label>
Normal Blazor binding:
<input @bind="InputValue" />
</label>
</p>

<p>
<label>
Demonstration of equivalent HTML binding:
<input value="@InputValue"
@onchange="@((ChangeEventArgs __e) => InputValue =
__e?.Value?.ToString())" />
</label>
</p>

<p>
<code>InputValue</code>: @InputValue
</p>

@code {
private string? InputValue { get; set; }
}

When the BindTheory component is rendered, the value of the HTML demonstration
<input> element comes from the InputValue property. When the user enters a value in

the text box and changes element focus, the onchange event is fired and the InputValue
property is set to the changed value. In reality, code execution is more complex because
@bind handles cases where type conversions are performed. In general, @bind
associates the current value of an expression with the value attribute of the <input>
and handles changes using the registered handler.

Bind a property or field on other Document Object Model (DOM) events by including
an @bind:event="{EVENT}" attribute with a DOM event for the {EVENT} placeholder. The
following example binds the InputValue property to the <input> element's value when
the element's oninput event (input ) is triggered. Unlike the onchange event
(change ), which fires when the element loses focus, oninput (input ) fires when the
value of the text box changes.

Page/BindEvent.razor :

razor

@page "/bind-event"

<p>
<input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
<code>InputValue</code>: @InputValue
</p>

@code {
private string? InputValue { get; set; }
}

To execute asynchronous logic after binding, use @bind:after="{EVENT}" with a DOM


event for the {EVENT} placeholder. An assigned C# method isn't executed until the
bound value is assigned synchronously.

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


ValueChanged { get; set; } ) with @bind:after isn't supported. Instead, pass a method

that returns an Action or Task to @bind:after .

In the following example:

The <input> element's value is bound to the value of searchText synchronously.


After each keystroke ( onchange event) in the field, the PerformSearch method
executes asynchronously.
PerformSearch calls a service with an asynchronous method ( FetchAsync ) to return
search results.

razor

@inject ISearchService SearchService

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

@code {
private string? searchText;
private string[]? searchResult;
private async Task PerformSearch()
{
searchResult = await SearchService.FetchAsync(searchText);
}
}

Additional examples

Pages/BindAfter.razor :

razor

@page "/bind-after"
@using Microsoft.AspNetCore.Components.Forms

<h1>Bind After Examples</h1>

<h2>Elements</h2>

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

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

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

<h2>Components</h2>

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

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

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

@code {
private string text = "";

private void After() {}


private Task AfterAsync() { return Task.CompletedTask; }
}

For more information on the InputText component, see ASP.NET Core Blazor forms and
input components.

Components 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.


Using an event callback parameter with @bind:set ( [Parameter] public
EventCallback<string> ValueChanged { get; set; } ) isn't supported. Instead, pass a
method that returns an Action or Task to @bind:set .

Examples

Pages/BindGetSet.razor :

razor

@page "/bind-get-set"
@using Microsoft.AspNetCore.Components.Forms

<h1>Bind Get Set Examples</h1>

<h2>Elements</h2>

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


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

<h2>Components</h2>

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


value; }" />
<InputText @bind-Value:get="text" @bind-Value:set="Set" />
<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

@code {
private string text = "";

private void Set(string value)


{
text = value;
}

private Task SetAsync(string value)


{
text = value;
return Task.CompletedTask;
}
}

For more information on the InputText component, see ASP.NET Core Blazor forms and
input components.

For another example use of @bind:get and @bind:set , see the Bind across more than
two components section later in this article.
Razor attribute binding is case-sensitive:

@bind , @bind:event , and @bind:after are valid.


@Bind / @bind:Event / @bind:aftEr (capital letters) or

@BIND / @BIND:EVENT / @BIND:AFTER (all capital letters) are invalid.

Use @bind:get / @bind:set modifiers and avoid


event handlers for two-way data binding
Two-way data binding isn't possible to implement with an event handler. Use
@bind:get / @bind:set modifiers for two-way data binding.

❌ Consider the following dysfunctional approach for two-way data binding using an
event handler:

razor

<p>
<input value="@inputValue" @oninput="OnInput" />
</p>

<p>
<code>inputValue</code>: @inputValue
</p>

@code {
private string? inputValue;

private void OnInput(ChangeEventArgs args)


{
var newValue = args.Value?.ToString() ?? string.Empty;

inputValue = newValue.Length > 4 ? "Long!" : newValue;


}
}

The OnInput event handler updates the value of inputValue to Long! after a fourth
character is provided. However, the user can continue adding characters to the element
value in the UI. The value of inputValue isn't bound back to the element's value with
each keystroke. The preceding example is only capable of one-way data binding.

The reason for this behavior is that Blazor isn't aware that your code intends to modify
the value of inputValue in the event handler. Blazor doesn't try to force Document
Object Model (DOM) element values and .NET variable values to match unless they're
bound with @bind syntax. In earlier versions of Blazor, two-way data binding is
implemented by binding the element to a property and controlling the property's value
with its setter. In ASP.NET Core 7.0 or later, @bind:get / @bind:set modifier syntax is used
to implement two-way data binding, as the next example demonstrates.

✔️Consider the following correct approach using @bind:get / @bind:set for two-way
data binding:

razor

<p>
<input @bind:event="oninput" @bind:get="inputValue" @bind:set="OnInput"
/>
</p>

<p>
<code>inputValue</code>: @inputValue
</p>

@code {
private string? inputValue;

private void OnInput(string value)


{
var newValue = value ?? string.Empty;

inputValue = newValue.Length > 4 ? "Long!" : newValue;


}
}

Using @bind:get / @bind:set modifiers both controls the underlying value of inputValue
via @bind:set and binds the value of inputValue to the element's value via @bind:get .
The preceding example demonstrates the correct approach for implementing two-way
data binding.

Binding to a property with C# get and set


accessors
C# get and set accessors can be used to create custom binding format behavior, as the
following DecimalBinding component demonstrates. The component binds a positive or
negative decimal with up to three decimal places to an <input> element by way of a
string property ( DecimalValue ).

Pages/DecimalBinding.razor :

razor
@page "/decimal-binding"
@using System.Globalization

<p>
<label>
Decimal value (±0.000 format):
<input @bind="DecimalValue" />
</label>
</p>

<p>
<code>decimalValue</code>: @decimalValue
</p>

@code {
private decimal decimalValue = 1.1M;
private NumberStyles style =
NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-
US");

private string DecimalValue


{
get => decimalValue.ToString("0.000", culture);
set
{
if (Decimal.TryParse(value, style, culture, out var number))
{
decimalValue = Math.Round(number, 3);
}
}
}
}

7 Note

Two-way binding to a property with get / set accessors requires discarding the
Task returned by EventCallback.InvokeAsync. For two-way data binding, we
recommend using @bind:get / @bind:set modifiers. For more information, see the
@bind:get / @bind:set guidance in the earlier in this article.

Multiple option selection with <select>


elements
Binding supports multiple option selection with <select> elements. The @onchange
event provides an array of the selected elements via event arguments
(ChangeEventArgs). The value must be bound to an array type.

Pages/BindMultipleInput.razor :

razor

@page "/bind-multiple-input"

<h1>Bind Multiple <code>input</code>Example</h1>

<p>
<label>
Select one or more cars:
<select @onchange="SelectedCarsChanged" multiple>
<option value="audi">Audi</option>
<option value="jeep">Jeep</option>
<option value="opel">Opel</option>
<option value="saab">Saab</option>
<option value="volvo">Volvo</option>
</select>
</label>
</p>

<p>
Selected Cars: @string.Join(", ", SelectedCars)
</p>

<p>
<label>
Select one or more cities:
<select @bind="SelectedCities" multiple>
<option value="bal">Baltimore</option>
<option value="la">Los Angeles</option>
<option value="pdx">Portland</option>
<option value="sf">San Francisco</option>
<option value="sea">Seattle</option>
</select>
</label>
</p>

<span>
Selected Cities: @string.Join(", ", SelectedCities)
</span>

@code {
public string[] SelectedCars { get; set; } = new string[] { };
public string[] SelectedCities { get; set; } = new[] { "bal", "sea" };

private void SelectedCarsChanged(ChangeEventArgs e)


{
if (e.Value is not null)
{
SelectedCars = (string[])e.Value;
}
}
}

For information on how empty strings and null values are handled in data binding, see
the Binding <select> element options to C# object null values section.

Binding <select> element options to C# object


null values
There's no sensible way to represent a <select> element option value as a C# object
null value, because:

HTML attributes can't have null values. The closest equivalent to null in HTML is
absence of the HTML value attribute from the <option> element.
When selecting an <option> with no value attribute, the browser treats the value
as the text content of that <option> 's element.

The Blazor framework doesn't attempt to suppress the default behavior because it
would involve:

Creating a chain of special-case workarounds in the framework.


Breaking changes to current framework behavior.

The most plausible null equivalent in HTML is an empty string value . The Blazor
framework handles null to empty string conversions for two-way binding to a
<select> 's value.

Unparsable values
When a user provides an unparsable value to a databound element, the unparsable
value is automatically reverted to its previous value when the bind event is triggered.

Consider the following component, where an <input> element is bound to an int type
with an initial value of 123 .

Pages/UnparsableValues.razor :

razor

@page "/unparseable-values"

<p>
<input @bind="inputValue" />
</p>

<p>
<code>inputValue</code>: @inputValue
</p>

@code {
private int inputValue = 123;
}

By default, binding applies to the element's onchange event. If the user updates the
value of the text box's entry to 123.45 and changes the focus, the element's value is
reverted to 123 when onchange fires. When the value 123.45 is rejected in favor of the
original value of 123 , the user understands that their value wasn't accepted.

For the oninput event ( @bind:event="oninput" ), a value reversion occurs after any
keystroke that introduces an unparsable value. When targeting the oninput event with
an int -bound type, a user is prevented from typing a dot ( . ) character. A dot ( . )
character is immediately removed, so the user receives immediate feedback that only
whole numbers are permitted. There are scenarios where reverting the value on the
oninput event isn't ideal, such as when the user should be allowed to clear an

unparsable <input> value. Alternatives include:

Don't use the oninput event. Use the default onchange event, where an invalid
value isn't reverted until the element loses focus.
Bind to a nullable type, such as int? or string and either use
@bind:get / @bind:set modifiers (described earlier in this article) or bind to a

property with custom get and set accessor logic to handle invalid entries.
Use a form validation component, such as InputNumber<TValue> or
InputDate<TValue>. Form validation components provide built-in support to
manage invalid inputs. Form validation components:
Permit the user to provide invalid input and receive validation errors on the
associated EditContext.
Display validation errors in the UI without interfering with the user entering
additional webform data.

Format strings
Data binding works with a single DateTime format string using @bind:format="{FORMAT
STRING}" , where the {FORMAT STRING} placeholder is the format string. Other format
expressions, such as currency or number formats, aren't available at this time but might
be added in a future release.

Pages/DateBinding.razor :

razor

@page "/date-binding"

<p>
<label>
<code>yyyy-MM-dd</code> format:
<input @bind="startDate" @bind:format="yyyy-MM-dd" />
</label>
</p>

<p>
<code>startDate</code>: @startDate
</p>

@code {
private DateTime startDate = new(2020, 1, 1);
}

In the preceding code, the <input> element's field type ( type attribute) defaults to
text .

Nullable System.DateTime and System.DateTimeOffset are supported:

C#

private DateTime? date;


private DateTimeOffset? dateOffset;

Specifying a format for the date field type isn't recommended because Blazor has built-
in support to format dates. In spite of the recommendation, only use the yyyy-MM-dd
date format for binding to function correctly if a format is supplied with the date field
type:

razor

<input type="date" @bind="startDate" @bind:format="yyyy-MM-dd">

Binding with component parameters


A common scenario is binding a property of a child component to a property in its
parent component. This scenario is called a chained bind because multiple levels of
binding occur simultaneously.

Component parameters permit binding properties of a parent component with @bind-


{PROPERTY} syntax, where the {PROPERTY} placeholder is the property to bind.

You can't implement chained binds with @bind syntax in the child component. An event
handler and value must be specified separately to support updating the property in the
parent from the child component.

The parent component still leverages the @bind syntax to set up the databinding with
the child component.

The following ChildBind component has a Year component parameter and an


EventCallback<TValue>. By convention, the EventCallback<TValue> for the parameter
must be named as the component parameter name with a " Changed " suffix. The naming
syntax is {PARAMETER NAME}Changed , where the {PARAMETER NAME} placeholder is the
parameter name. In the following example, the EventCallback<TValue> is named
YearChanged .

EventCallback.InvokeAsync invokes the delegate associated with the binding with the
provided argument and dispatches an event notification for the changed property.

Shared/ChildBind.razor :

razor

<div class="card bg-light mt-3" style="width:18rem ">


<div class="card-body">
<h3 class="card-title">ChildBind Component</h3>
<p class="card-text">
Child <code>Year</code>: @Year
</p>
<button @onclick="UpdateYearFromChild">Update Year from
Child</button>
</div>
</div>

@code {
private Random r = new();

[Parameter]
public int Year { get; set; }

[Parameter]
public EventCallback<int> YearChanged { get; set; }
private async Task UpdateYearFromChild()
{
await YearChanged.InvokeAsync(r.Next(1950, 2021));
}
}

For more information on events and EventCallback<TValue>, see the EventCallback


section of the ASP.NET Core Blazor event handling article.

In the following Parent1 component, the year field is bound to the Year parameter of
the child component. The Year parameter is bindable because it has a companion
YearChanged event that matches the type of the Year parameter.

Pages/Parent1.razor :

razor

@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
private Random r = new();
private int year = 1979;

private void UpdateYear()


{
year = r.Next(1950, 2021);
}
}

Component parameter binding can also trigger @bind:after events. In the following
example, the YearUpdated method executes asynchronously after binding the Year
component parameter.

razor

<ChildBind @bind-Year="year" @bind-Year:after="YearUpdated" />

@code {
...

private async Task YearUpdated()


{
... = await ...;
}
}

By convention, a property can be bound to a corresponding event handler by including


an @bind-{PROPERTY}:event attribute assigned to the handler, where the {PROPERTY}
placeholder is the property. <ChildBind @bind-Year="year" /> is equivalent to writing:

razor

<ChildBind @bind-Year="year" @bind-Year:event="YearChanged" />

In a more sophisticated and real-world example, the following PasswordEntry


component:

Sets an <input> element's value to a password field.


Exposes changes of a Password property to a parent component with an
EventCallback that passes in the current value of the child's password field as its
argument.
Uses the onclick event to trigger the ToggleShowPassword method. For more
information, see ASP.NET Core Blazor event handling.

Shared/PasswordEntry.razor :

razor

<div class="card bg-light mt-3" style="width:22rem ">


<div class="card-body">
<h3 class="card-title">Password Component</h3>
<p class="card-text">
<label>
Password:
<input @oninput="OnPasswordChanged"
required
type="@(showPassword ? "text" : "password")"
value="@password" />
</label>
</p>
<button class="btn btn-primary" @onclick="ToggleShowPassword">
Show password
</button>
</div>
</div>

@code {
private bool showPassword;
private string? password;
[Parameter]
public string? Password { get; set; }

[Parameter]
public EventCallback<string> PasswordChanged { get; set; }

private async Task OnPasswordChanged(ChangeEventArgs e)


{
password = e?.Value?.ToString();

await PasswordChanged.InvokeAsync(password);
}

private void ToggleShowPassword()


{
showPassword = !showPassword;
}
}

The PasswordEntry component is used in another component, such as the following


PasswordBinding component example.

Pages/PasswordBinding.razor :

razor

@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
<code>password</code>: @password
</p>

@code {
private string password = "Not set";
}

When the PasswordBinding component is initially rendered, the password value of Not
set is displayed in the UI. After initial rendering, the value of password reflects changes
made to the Password component parameter value in the PasswordEntry component.

7 Note
The preceding example binds the password one-way from the child PasswordEntry
component to the parent PasswordBinding component. Two-way binding isn't a
requirement in this scenario if the goal is for the app to have a shared password
entry component for reuse around the app that merely passes the password to the
parent. For an approach that permits two-way binding without writing directly to
the child component's parameter, see the NestedChild component example in the
Bind across more than two components section of this article.

Perform checks or trap errors in the handler. The following revised PasswordEntry
component provides immediate feedback to the user if a space is used in the
password's value.

Shared/PasswordEntry.razor :

razor

<div class="card bg-light mt-3" style="width:22rem ">


<div class="card-body">
<h3 class="card-title">Password Component</h3>
<p class="card-text">
<label>
Password:
<input @oninput="OnPasswordChanged"
required
type="@(showPassword ? "text" : "password")"
value="@password" />
</label>
<span class="text-danger">@validationMessage</span>
</p>
<button class="btn btn-primary" @onclick="ToggleShowPassword">
Show password
</button>
</div>
</div>

@code {
private bool showPassword;
private string? password;
private string? validationMessage;

[Parameter]
public string? Password { get; set; }

[Parameter]
public EventCallback<string> PasswordChanged { get; set; }

private Task OnPasswordChanged(ChangeEventArgs e)


{
password = e?.Value?.ToString();
if (password != null && password.Contains(' '))
{
validationMessage = "Spaces not allowed!";

return Task.CompletedTask;
}
else
{
validationMessage = string.Empty;

return PasswordChanged.InvokeAsync(password);
}
}

private void ToggleShowPassword()


{
showPassword = !showPassword;
}
}

In the following example, the PasswordUpdated method executes asynchronously after


binding the Password component parameter:

razor

<PasswordEntry @bind-Password="password" @bind-


Password:after="PasswordUpdated" />

Bind across more than two components


You can bind parameters through any number of nested components, but you must
respect the one-way flow of data:

Change notifications flow up the hierarchy.


New parameter values flow down the hierarchy.

A common and recommended approach is to only store the underlying data in the
parent component to avoid any confusion about what state must be updated, as shown
in the following example.

Pages/Parent2.razor :

razor

@page "/parent-2"
<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
<button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
private string parentMessage = "Initial value set in Parent";

private void ChangeValue()


{
parentMessage = $"Set in Parent {DateTime.Now}";
}
}

In the following NestedChild component, the NestedGrandchild component:

Assigns the value of ChildMessage to GrandchildMessage with @bind:get syntax.


Updates GrandchildMessage when ChildMessageChanged executes with @bind:set
syntax.

Shared/NestedChild.razor :

razor

<div class="border rounded m-1 p-1">


<h2>Child Component</h2>

<p>Child Message: <b>@ChildMessage</b></p>

<p>
<button @onclick="ChangeValue">Change from Child</button>
</p>

<NestedGrandchild @bind-GrandchildMessage:get="ChildMessage"
@bind-GrandchildMessage:set="ChildMessageChanged" />
</div>

@code {
[Parameter]
public string? ChildMessage { get; set; }

[Parameter]
public EventCallback<string> ChildMessageChanged { get; set; }

private async Task ChangeValue()


{
await ChildMessageChanged.InvokeAsync(
$"Set in Child {DateTime.Now}");
}
}

Shared/NestedGrandchild.razor :

razor

<div class="border rounded m-1 p-1">


<h3>Grandchild Component</h3>

<p>Grandchild Message: <b>@GrandchildMessage</b></p>

<p>
<button @onclick="ChangeValue">Change from Grandchild</button>
</p>
</div>

@code {
[Parameter]
public string? GrandchildMessage { get; set; }

[Parameter]
public EventCallback<string> GrandchildMessageChanged { get; set; }

private async Task ChangeValue()


{
await GrandchildMessageChanged.InvokeAsync(
$"Set in Grandchild {DateTime.Now}");
}
}

For an alternative approach suited to sharing data in memory and across components
that aren't necessarily nested, see ASP.NET Core Blazor state management.

Additional resources
Parameter change detection and additional guidance on Razor component
rendering
ASP.NET Core Blazor forms and input components
Binding to radio buttons in a form
Binding InputSelect options to C# object null values
ASP.NET Core Blazor event handling: EventCallback section
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor event handling
Article • 06/07/2023

This article explains Blazor's event handling features, including event argument types,
event callbacks, and managing default browser events.

Specify delegate event handlers in Razor component markup with @on{DOM EVENT}="
{DELEGATE}" Razor syntax:

The {DOM EVENT} placeholder is a Document Object Model (DOM) event (for
example, click ).
The {DELEGATE} placeholder is the C# delegate event handler.

For event handling:

Asynchronous delegate event handlers that return a Task are supported.


Delegate event handlers automatically trigger a UI render, so there's no need to
manually call StateHasChanged.
Exceptions are logged.

The following code:

Calls the UpdateHeading method when the button is selected in the UI.
Calls the CheckChanged method when the checkbox is changed in the UI.

Pages/EventHandlerExample1.razor :

razor

@page "/event-handler-example-1"

<h1>@currentHeading</h1>

<p>
<label>
New title
<input @bind="newHeading" />
</label>
<button @onclick="UpdateHeading">
Update heading
</button>
</p>

<p>
<label>
<input type="checkbox" @onchange="CheckChanged" />
@checkedMessage
</label>
</p>

@code {
private string currentHeading = "Initial heading";
private string? newHeading;
private string checkedMessage = "Not changed yet";

private void UpdateHeading()


{
currentHeading = $"{newHeading}!!!";
}

private void CheckChanged()


{
checkedMessage = $"Last changed at {DateTime.Now}";
}
}

In the following example, UpdateHeading :

Is called asynchronously when the button is selected.


Waits two seconds before updating the heading.

Pages/EventHandlerExample2.razor :

razor

@page "/event-handler-example-2"

<h1>@currentHeading</h1>

<p>
<label>
New title
<input @bind="newHeading" />
</label>
<button @onclick="UpdateHeading">
Update heading
</button>
</p>

@code {
private string currentHeading = "Initial heading";
private string? newHeading;
private async Task UpdateHeading()
{
await Task.Delay(2000);

currentHeading = $"{newHeading}!!!";
}
}

Built-in event arguments


For events that support an event argument type, specifying an event parameter in the
event method definition is only necessary if the event type is used in the method. In the
following example, MouseEventArgs is used in the ReportPointerLocation method to set
message text that reports the mouse coordinates when the user selects a button in the
UI.

Pages/EventHandlerExample3.razor :

razor

@page "/event-handler-example-3"

@for (var i = 0; i < 4; i++)


{
<p>
<button @onclick="ReportPointerLocation">
Where's my mouse pointer for this button?
</button>
</p>
}

<p>@mousePointerMessage</p>

@code {
private string? mousePointerMessage;

private void ReportPointerLocation(MouseEventArgs e)


{
mousePointerMessage = $"Mouse coordinates: {e.ScreenX}:{e.ScreenY}";
}
}

Supported EventArgs are shown in the following table.

Event Class Document Object Model (DOM) notes

Clipboard ClipboardEventArgs
Event Class Document Object Model (DOM) notes

Drag DragEventArgs DataTransfer and DataTransferItem hold dragged item data.

Implement drag and drop in Blazor apps using JS interop with


HTML Drag and Drop API .

Error ErrorEventArgs

Event EventArgs EventHandlers holds attributes to configure the mappings


between event names and event argument types.

Focus FocusEventArgs Doesn't include support for relatedTarget .

Input ChangeEventArgs

Keyboard KeyboardEventArgs

Mouse MouseEventArgs

Mouse PointerEventArgs
pointer

Mouse WheelEventArgs
wheel

Progress ProgressEventArgs

Touch TouchEventArgs TouchPoint represents a single contact point on a touch-


sensitive device.

For more information, see the following resources:

EventArgs classes in the ASP.NET Core reference source (dotnet/aspnetcore main


branch)

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

EventHandlers holds attributes to configure the mappings between event names


and event argument types.
Custom event arguments
Blazor supports custom event arguments, which enable you to pass arbitrary data to
.NET event handlers with custom events.

General configuration
Custom events with custom event arguments are generally enabled with the following
steps.

1. In JavaScript, define a function for building the custom event argument object
from the source event:

JavaScript

function eventArgsCreator(event) {
return {
customProperty1: 'any value for property 1',
customProperty2: event.srcElement.value
};
}

2. Register the custom event with the preceding handler in a JavaScript initializer.

wwwroot/{PACKAGE ID/ASSEMBLY NAME}.lib.module.js :

JavaScript

export function afterStarted(blazor) {


blazor.registerCustomEventType('customevent', {
createEventArgs: eventArgsCreator
});
}

In the preceding example, the {PACKAGE ID/ASSEMBLY NAME} placeholder of the file
name represents the package ID or assembly name of the app.

7 Note

The call to registerCustomEventType is performed in a script only once per


event.

For the call to registerCustomEventType , use the blazor parameter (lowercase


b ) provided by afterStarted . Although the registration is valid when using
the Blazor object (uppercase B ), the preferred approach is to use the
parameter.

3. Define a class for the event arguments:

C#

namespace BlazorSample.CustomEvents;

public class CustomEventArgs : EventArgs


{
public string? CustomProperty1 {get; set;}
public string? CustomProperty2 {get; set;}
}

4. Wire up the custom event with the event arguments by adding an


EventHandlerAttribute attribute annotation for the custom event:

In order for the compiler to find the [EventHandler] class, it must be placed
into a C# class file ( .cs ), making it a normal top-level class.
Mark the class public .
The class doesn't require members.
The class must be called " EventHandlers " in order to be found by the Razor
compiler.
Place the class under a namespace specific to your app.
Import the namespace into the Razor component ( .razor ) where the event is
used.

C#

using Microsoft.AspNetCore.Components;

namespace BlazorSample.CustomEvents;

[EventHandler("oncustomevent", typeof(CustomEventArgs),
enableStopPropagation: true, enablePreventDefault: true)]
public static class EventHandlers
{
}

5. Register the event handler on one or more HTML elements. Access the data that
was passed in from JavaScript in the delegate handler method:

razor
@using namespace BlazorSample.CustomEvents

<button @oncustomevent="HandleCustomEvent">Handle</button>

@code
{
private void HandleCustomEvent(CustomEventArgs eventArgs)
{
// eventArgs.CustomProperty1
// eventArgs.CustomProperty2
}
}

If the @oncustomevent attribute isn't recognized by IntelliSense, make sure that the
component or the _Imports.razor file contains an @using statement for the namespace
containing the EventHandler class.

Whenever the custom event is fired on the DOM, the event handler is called with the
data passed from the JavaScript.

If you're attempting to fire a custom event, bubbles must be enabled by setting its
value to true . Otherwise, the event doesn't reach the Blazor handler for processing into
the C# custom EventHandlerAttribute method. For more information, see MDN Web
Docs: Event bubbling .

Custom clipboard paste event example


The following example receives a custom clipboard paste event that includes the time of
the paste and the user's pasted text.

Declare a custom name ( oncustompaste ) for the event and a .NET class
( CustomPasteEventArgs ) to hold the event arguments for this event:

CustomEvents.cs :

C#

using Microsoft.AspNetCore.Components;

namespace BlazorSample.CustomEvents;

[EventHandler("oncustompaste", typeof(CustomPasteEventArgs),
enableStopPropagation: true, enablePreventDefault: true)]
public static class EventHandlers
{
}
public class CustomPasteEventArgs : EventArgs
{
public DateTime EventTimestamp { get; set; }
public string? PastedData { get; set; }
}

Add JavaScript code to supply data for the EventArgs subclass with the preceding
handler in a JavaScript initializer. The following example only handles pasting text, but
you could use arbitrary JavaScript APIs to deal with users pasting other types of data,
such as images.

wwwroot/{PACKAGE ID/ASSEMBLY NAME}.lib.module.js :

JavaScript

export function afterStarted(blazor) {


blazor.registerCustomEventType('custompaste', {
browserEventName: 'paste',
createEventArgs: event => {
return {
eventTimestamp: new Date(),
pastedData: event.clipboardData.getData('text')
};
}
});
}

In the preceding example, the {PACKAGE ID/ASSEMBLY NAME} placeholder of the file name
represents the package ID or assembly name of the app.

7 Note

For the call to registerCustomEventType , use the blazor parameter (lowercase b )


provided by afterStarted . Although the registration is valid when using the Blazor
object (uppercase B ), the preferred approach is to use the parameter.

The preceding code tells the browser that when a native paste event occurs:

Raise a custompaste event.


Supply the event arguments data using the custom logic stated:
For the eventTimestamp , create a new date.
For the pastedData , get the clipboard data as text. For more information, see
MDN Web Docs: ClipboardEvent.clipboardData .
Event name conventions differ between .NET and JavaScript:

In .NET, event names are prefixed with " on ".


In JavaScript, event names don't have a prefix.

In a Razor component, attach the custom handler to an element.

Pages/CustomPasteArguments.razor :

razor

@page "/custom-paste-arguments"
@using BlazorSample.CustomEvents

<label>
Try pasting into the following text box:
<input @oncustompaste="HandleCustomPaste" />
</label>

<p>
@message
</p>

@code {
private string? message;

private void HandleCustomPaste(CustomPasteEventArgs eventArgs)


{
message = $"At {eventArgs.EventTimestamp.ToShortTimeString()}, " +
$"you pasted: {eventArgs.PastedData}";
}
}

Lambda expressions
Lambda expressions are supported as the delegate event handler.

Pages/EventHandlerExample4.razor :

razor

@page "/event-handler-example-4"

<h1>@heading</h1>

<p>
<button @onclick="@(e => heading = "New heading!!!")">
Update heading
</button>
</p>

@code {
private string heading = "Initial heading";
}

It's often convenient to close over additional values using C# method parameters, such
as when iterating over a set of elements. The following example creates three buttons,
each of which calls UpdateHeading and passes the following data:

An event argument (MouseEventArgs) in e .


The button number in buttonNumber .

Pages/EventHandlerExample5.razor :

razor

@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)


{
var buttonNumber = i;

<p>
<button @onclick="@(e => UpdateHeading(e, buttonNumber))">
Button #@i
</button>
</p>
}

@code {
private string heading = "Select a button to learn its position";

private void UpdateHeading(MouseEventArgs e, int buttonNumber)


{
heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
}
}

Creating a large number of event delegates in a loop may cause poor rendering
performance. For more information, see ASP.NET Core Blazor performance best
practices.

Avoid using a loop variable directly in a lambda expression, such as i in the preceding
for loop example. Otherwise, the same variable is used by all lambda expressions,
which results in use of the same value in all lambdas. Capture the variable's value in a
local variable. In the preceding example:

The loop variable i is assigned to buttonNumber .


buttonNumber is used in the lambda expression.

Alternatively, use a foreach loop with Enumerable.Range, which doesn't suffer from the
preceding problem:

razor

@foreach (var buttonNumber in Enumerable.Range(1,3))


{
<p>
<button @onclick="@(e => UpdateHeading(e, buttonNumber))">
Button #@buttonNumber
</button>
</p>
}

EventCallback
A common scenario with nested components executes a parent component's method
when a child component event occurs. An onclick event occurring in the child
component is a common use case. To expose events across components, use an
EventCallback. A parent component can assign a callback method to a child
component's EventCallback.

The following Child component demonstrates how a button's onclick handler is set up
to receive an EventCallback delegate from the sample's ParentComponent . The
EventCallback is typed with MouseEventArgs , which is appropriate for an onclick event
from a peripheral device.

Shared/Child.razor :

razor

<p>
<button @onclick="OnClickCallback">
Trigger a Parent component method
</button>
</p>

@code {
[Parameter]
public string? Title { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }

[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}

The Parent component sets the child's EventCallback<TValue> ( OnClickCallback ) to its


ShowMessage method.

Pages/Parent.razor :

razor

@page "/parent"

<h1>Parent-child example</h1>

<Child Title="Panel Title from Parent" OnClickCallback="@ShowMessage">


Content of the child component is supplied by the parent component.
</Child>

<p>@message</p>

@code {
private string? message;

private void ShowMessage(MouseEventArgs e)


{
message = $"Blaze a new trail with Blazor! ({e.ScreenX}:
{e.ScreenY})";
}
}

When the button is selected in the ChildComponent :

The Parent component's ShowMessage method is called. message is updated and


displayed in the Parent component.
A call to StateHasChanged isn't required in the callback's method ( ShowMessage ).
StateHasChanged is called automatically to rerender the Parent component, just
as child events trigger component rerendering in event handlers that execute
within the child. For more information, see ASP.NET Core Razor component
rendering.

EventCallback and EventCallback<TValue> permit asynchronous delegates.


EventCallback is weakly typed and allows passing any type argument in
InvokeAsync(Object) . EventCallback<TValue> is strongly typed and requires passing a T

argument in InvokeAsync(T) that's assignable to TValue .

razor

<ChildComponent
OnClickCallback="@(async () => { await Task.Yield(); messageText =
"Blaze It!"; })" />

Invoke an EventCallback or EventCallback<TValue> with InvokeAsync and await the Task:

C#

await OnClickCallback.InvokeAsync(arg);

Use EventCallback and EventCallback<TValue> for event handling and binding


component parameters.

Prefer the strongly typed EventCallback<TValue> over EventCallback.


EventCallback<TValue> provides enhanced error feedback to users of the component.
Similar to other UI event handlers, specifying the event parameter is optional. Use
EventCallback when there's no value passed to the callback.

Prevent default actions


Use the @on{DOM EVENT}:preventDefault directive attribute to prevent the default
action for an event, where the {DOM EVENT} placeholder is a Document Object Model
(DOM) event .

When a key is selected on an input device and the element focus is on a text box, a
browser normally displays the key's character in the text box. In the following example,
the default behavior is prevented by specifying the @onkeydown:preventDefault directive
attribute. When the focus is on the <input> element, the counter increments with the
key sequence Shift + + . The + character isn't assigned to the <input> element's value.
For more information on keydown , see MDN Web Docs: Document: keydown event .

Pages/EventHandlerExample6.razor :

razor

@page "/event-handler-example-6"

<p>
<input value="@count" @onkeydown="KeyHandler" @onkeydown:preventDefault
/>
</p>

@code {
private int count = 0;

private void KeyHandler(KeyboardEventArgs e)


{
if (e.Key == "+")
{
count++;
}
}
}

Specifying the @on{DOM EVENT}:preventDefault attribute without a value is equivalent to


@on{DOM EVENT}:preventDefault="true" .

An expression is also a permitted value of the attribute. In the following example,


shouldPreventDefault is a bool field set to either true or false :

razor

<input @onkeydown:preventDefault="shouldPreventDefault" />

...

@code {
private bool shouldPreventDefault = true;
}

Stop event propagation


Use the @on{DOM EVENT}:stopPropagation directive attribute to stop event
propagation within the Blazor scope. {DOM EVENT} is a placeholder for a Document
Object Model (DOM) event .

The stopPropagation directive attribute's effect is limited to the Blazor scope and
doesn't extend to the HTML DOM. Events must propagate to the HTML DOM root
before Blazor can act upon them. For a mechanism to prevent HTML DOM event
propagation, consider the following approach:

Obtain the event's path by calling Event.composedPath() .


Filter events based on the composed event targets (EventTarget) .
In the following example, selecting the checkbox prevents click events from the second
child <div> from propagating to the parent <div> . Since propagated click events
normally fire the OnSelectParentDiv method, selecting the second child <div> results in
the parent <div> message appearing unless the checkbox is selected.

Pages/EventHandlerExample7.razor :

razor

@page "/event-handler-example-7"

<label>
<input @bind="stopPropagation" type="checkbox" />
Stop Propagation
</label>

<div class="m-1 p-1 border border-primary" @onclick="OnSelectParentDiv">


<h3>Parent div</h3>

<div class="m-1 p-1 border" @onclick="OnSelectChildDiv">


Child div that doesn't stop propagation when selected.
</div>

<div class="m-1 p-1 border" @onclick="OnSelectChildDiv"


@onclick:stopPropagation="stopPropagation">
Child div that stops propagation when selected.
</div>
</div>

<p>
@message
</p>

@code {
private bool stopPropagation = false;
private string? message;

private void OnSelectParentDiv() =>


message = $"The parent div was selected. {DateTime.Now}";

private void OnSelectChildDiv() =>


message = $"A child div was selected. {DateTime.Now}";
}

Focus an element
Call FocusAsync on an element reference to focus an element in code. In the following
example, select the button to focus the <input> element.
Pages/EventHandlerExample8.razor :

razor

@page "/event-handler-example-8"

<p>
<input @ref="exampleInput" />
</p>

<button @onclick="ChangeFocus">
Focus the Input Element
</button>

@code {
private ElementReference exampleInput;

private async Task ChangeFocus()


{
await exampleInput.FocusAsync();
}
}
ASP.NET Core Razor component lifecycle
Article • 06/07/2023

This article explains the ASP.NET Core Razor component lifecycle and how to use
lifecycle events.

The Razor component processes Razor component lifecycle events in a set of


synchronous and asynchronous lifecycle methods. The lifecycle methods can be
overridden to perform additional operations in components during component
initialization and rendering.

This article simplifies component lifecycle event processing in order to clarify complex
framework logic. You may need to access the ComponentBase reference source to
integrate custom event processing with Blazor's lifecycle event processing. Code
comments in the reference source include additional remarks on lifecycle event
processing that don't appear in this article or in the API documentation. Blazor's lifecycle
event processing has changed over time and is subject to change without notice each
release.

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Lifecycle events
The following simplified diagrams illustrate Razor component lifecycle event processing.
The C# methods associated with the lifecycle events are defined with examples in the
following sections of this article.

Component lifecycle events:

1. If the component is rendering for the first time on a request:

Create the component's instance.


Perform property injection. Run SetParametersAsync.
Call OnInitialized{Async}. If an incomplete Task is returned, the Task is awaited
and then the component is rerendered.

2. Call OnParametersSet{Async}. If an incomplete Task is returned, the Task is awaited


and then the component is rerendered.
3. Render for all synchronous work and complete Tasks.

7 Note

Asynchronous actions performed in lifecycle events might not have completed


before a component is rendered. For more information, see the Handle incomplete
async actions at render section later in this article.

A parent component renders before its children components because rendering is what
determines which children are present. If synchronous parent component initialization is
used, the parent initialization is guaranteed to complete first. If asynchronous parent
component initialization is used, the completion order of parent and child component
initialization can't be determined because it depends on the initialization code running.
Document Object Model (DOM) event processing:

1. The event handler is run.


2. If an incomplete Task is returned, the Task is awaited and then the component is
rerendered.
3. Render for all synchronous work and complete Tasks.
The Render lifecycle:

1. Avoid further rendering operations on the component:

After the first render.


When ShouldRender is false .

2. Build the render tree diff (difference) and render the component.
3. Await the DOM to update.
4. Call OnAfterRender{Async}.
Developer calls to StateHasChanged result in a render. For more information, see
ASP.NET Core Razor component rendering.

When parameters are set ( SetParametersAsync )


SetParametersAsync sets parameters supplied by the component's parent in the render
tree or from route parameters.

The method's ParameterView parameter contains the set of component parameter


values for the component each time SetParametersAsync is called. By overriding the
SetParametersAsync method, developer code can interact directly with ParameterView's
parameters.

The default implementation of SetParametersAsync sets the value of each property with
the [Parameter] or [CascadingParameter] attribute that has a corresponding value in the
ParameterView. Parameters that don't have a corresponding value in ParameterView are
left unchanged.

If base.SetParametersAsync isn't invoked, developer code can interpret the incoming


parameters' values in any way required. For example, there's no requirement to assign
the incoming parameters to the properties of the class.

If event handlers are provided in developer code, unhook them on disposal. For more
information, see the Component disposal with IDisposable IAsyncDisposable section.

In the following example, ParameterView.TryGetValue assigns the Param parameter's


value to value if parsing a route parameter for Param is successful. When value isn't
null , the value is displayed by the component.

Although route parameter matching is case insensitive, TryGetValue only matches case-
sensitive parameter names in the route template. The following example requires the
use of /{Param?} in the route template in order to get the value with TryGetValue, not
/{param?} . If /{param?} is used in this scenario, TryGetValue returns false and message
isn't set to either message string.

Pages/SetParamsAsync.razor :

razor

@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
private string message = "Not set";

[Parameter]
public string? Param { get; set; }

public override async Task SetParametersAsync(ParameterView parameters)


{
if (parameters.TryGetValue<string>(nameof(Param), out var value))
{
if (value is null)
{
message = "The value of 'Param' is null.";
}
else
{
message = $"The value of 'Param' is {value}.";
}
}

await base.SetParametersAsync(parameters);
}
}

Component initialization
( OnInitialized{Async} )
OnInitialized and OnInitializedAsync are invoked when the component is initialized after
having received its initial parameters in SetParametersAsync.

If synchronous parent component initialization is used, the parent initialization is


guaranteed to complete before child component initialization. If asynchronous parent
component initialization is used, the completion order of parent and child component
initialization can't be determined because it depends on the initialization code running.

For a synchronous operation, override OnInitialized:

Pages/OnInit.razor :

razor

@page "/on-init"

<p>@message</p>

@code {
private string? message;

protected override void OnInitialized()


{
message = $"Initialized at {DateTime.Now}";
}
}

To perform an asynchronous operation, override OnInitializedAsync and use the await


operator:

C#

protected override async Task OnInitializedAsync()


{
await ...
}

Blazor apps that prerender their content on the server call OnInitializedAsync twice:
Once when the component is initially rendered statically as part of the page.
A second time when the browser renders the component.

To prevent developer code in OnInitializedAsync from running twice when prerendering,


see the Stateful reconnection after prerendering section. Although the content in the
section focuses on Blazor Server and stateful SignalR reconnection, the scenario for
prerendering in hosted Blazor WebAssembly apps (WebAssemblyPrerendered) involves
similar conditions and approaches to prevent executing developer code twice. To
preserve state during the execution of initialization code while prerendering, see
Prerender and integrate ASP.NET Core Razor components.

While a Blazor app is prerendering, certain actions, such as calling into JavaScript (JS
interop), aren't possible. Components may need to render differently when prerendered.
For more information, see the Prerendering with JavaScript interop section.

If event handlers are provided in developer code, unhook them on disposal. For more
information, see the Component disposal with IDisposable IAsyncDisposable section.

After parameters are set


( OnParametersSet{Async} )
OnParametersSet or OnParametersSetAsync are called:

After the component is initialized in OnInitialized or OnInitializedAsync.

When the parent component rerenders and supplies:


Known or primitive immutable types when at least one parameter has changed.
Complex-typed parameters. The framework can't know whether the values of a
complex-typed parameter have mutated internally, so the framework always
treats the parameter set as changed when one or more complex-typed
parameters are present.

For more information on rendering conventions, see ASP.NET Core Razor


component rendering.

For the following example component, navigate to the component's page at a URL:

With a start date that's received by StartDate : /on-parameters-set/2021-03-19


Without a start date, where StartDate is assigned a value of the current local time:
/on-parameters-set

7 Note
In a component route, it isn't possible to both constrain a DateTime parameter
with the route constraint datetime and make the parameter optional. Therefore,
the following OnParamsSet component uses two @page directives to handle
routing with and without a supplied date segment in the URL.

Pages/OnParamsSet.razor :

razor

@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
private string? message;

[Parameter]
public DateTime StartDate { get; set; }

protected override void OnParametersSet()


{
if (StartDate == default)
{
StartDate = DateTime.Now;

message = $"No start date in URL. Default value applied


(StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used (StartDate:
{StartDate}).";
}
}
}

Asynchronous work when applying parameters and property values must occur during
the OnParametersSetAsync lifecycle event:

C#

protected override async Task OnParametersSetAsync()


{
await ...
}
If event handlers are provided in developer code, unhook them on disposal. For more
information, see the Component disposal with IDisposable IAsyncDisposable section.

For more information on route parameters and constraints, see ASP.NET Core Blazor
routing and navigation.

After component render


( OnAfterRender{Async} )
OnAfterRender and OnAfterRenderAsync are called after a component has finished
rendering. Element and component references are populated at this point. Use this
stage to perform additional initialization steps with the rendered content, such as JS
interop calls that interact with the rendered DOM elements.

The firstRender parameter for OnAfterRender and OnAfterRenderAsync:

Is set to true the first time that the component instance is rendered.
Can be used to ensure that initialization work is only performed once.

Pages/AfterRender.razor :

razor

@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger

<button @onclick="LogInformation">Log information (and trigger a render)


</button>

@code {
private string message = "Initial assigned message.";

protected override void OnAfterRender(bool firstRender)


{
Logger.LogInformation("OnAfterRender(1): firstRender: " +
"{FirstRender}, message: {Message}", firstRender, message);

if (firstRender)
{
message = "Executed for the first render.";
}
else
{
message = "Executed after the first render.";
}

Logger.LogInformation("OnAfterRender(2): firstRender: " +


"{FirstRender}, message: {Message}", firstRender, message);
}

private void LogInformation()


{
Logger.LogInformation("LogInformation called");
}
}

Asynchronous work immediately after rendering must occur during the


OnAfterRenderAsync lifecycle event:

C#

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
await ...
}
}

Even if you return a Task from OnAfterRenderAsync, the framework doesn't schedule a
further render cycle for your component once that task completes. This is to avoid an
infinite render loop. This is different from the other lifecycle methods, which schedule a
further render cycle once a returned Task completes.

OnAfterRender and OnAfterRenderAsync aren't called during the prerendering process on


the server. The methods are called when the component is rendered interactively after
prerendering. When the app prerenders:

1. The component executes on the server to produce some static HTML markup in
the HTTP response. During this phase, OnAfterRender and OnAfterRenderAsync
aren't called.
2. When the Blazor script ( blazor.webassembly.js or blazor.server.js ) start in the
browser, the component is restarted in an interactive rendering mode. After a
component is restarted, OnAfterRender and OnAfterRenderAsync are called
because the app isn't in the prerendering phase any longer.

If event handlers are provided in developer code, unhook them on disposal. For more
information, see the Component disposal with IDisposable IAsyncDisposable section.

State changes ( StateHasChanged )


StateHasChanged notifies the component that its state has changed. When applicable,
calling StateHasChanged causes the component to be rerendered.

StateHasChanged is called automatically for EventCallback methods. For more


information on event callbacks, see ASP.NET Core Blazor event handling.

For more information on component rendering and when to call StateHasChanged,


including when to invoke it with ComponentBase.InvokeAsync, see ASP.NET Core Razor
component rendering.

Handle incomplete async actions at render


Asynchronous actions performed in lifecycle events might not have completed before
the component is rendered. Objects might be null or incompletely populated with data
while the lifecycle method is executing. Provide rendering logic to confirm that objects
are initialized. Render placeholder UI elements (for example, a loading message) while
objects are null .

In the FetchData component of the Blazor templates, OnInitializedAsync is overridden to


asynchronously receive forecast data ( forecasts ). When forecasts is null , a loading
message is displayed to the user. After the Task returned by OnInitializedAsync
completes, the component is rerendered with the updated state.

Pages/FetchData.razor in the Blazor Server template:

razor

@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)


{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<!-- forecast data in table element content -->
</table>
}

@code {
private WeatherForecast[]? forecasts;

protected override async Task OnInitializedAsync()


{
forecasts = await
ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
}
}

Handle errors
For information on handling errors during lifecycle method execution, see Handle errors
in ASP.NET Core Blazor apps.

Stateful reconnection after prerendering


In a Blazor Server app when RenderMode is ServerPrerendered, the component is
initially rendered statically as part of the page. Once the browser establishes a SignalR
connection back to the server, the component is rendered again and interactive. If the
OnInitialized{Async} lifecycle method for initializing the component is present, the
method is executed twice:

When the component is prerendered statically.


After the server connection has been established.

This can result in a noticeable change in the data displayed in the UI when the
component is finally rendered. To avoid this double-rendering behavior in a Blazor
Server app, pass in an identifier to cache the state during prerendering and to retrieve
the state after prerendering.

The following code demonstrates an updated WeatherForecastService in a template-


based Blazor Server app that avoids the double rendering. In the following example, the
awaited Delay ( await Task.Delay(...) ) simulates a short delay before returning data
from the GetForecastAsync method.

WeatherForecastService.cs :

C#

using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService


{
private static readonly string[] summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

public WeatherForecastService(IMemoryCache memoryCache)


{
MemoryCache = memoryCache;
}

public IMemoryCache MemoryCache { get; }

public Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate)


{
return MemoryCache.GetOrCreateAsync(startDate, async e =>
{
e.SetOptions(new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(30)
});

var rng = new Random();

await Task.Delay(TimeSpan.FromSeconds(10));

return Enumerable.Range(1, 5).Select(index => new


WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = summaries[rng.Next(summaries.Length)]
}).ToArray();
});
}
}

For more information on the RenderMode, see ASP.NET Core Blazor SignalR guidance.

Although the content in this section focuses on Blazor Server and stateful SignalR
reconnection, the scenario for prerendering in hosted Blazor WebAssembly apps
(WebAssemblyPrerendered) involves similar conditions and approaches to prevent
executing developer code twice. To preserve state during the execution of initialization
code while prerendering, see Prerender and integrate ASP.NET Core Razor components.

Prerendering with JavaScript interop


This section applies to Blazor Server and hosted Blazor WebAssembly apps that prerender
Razor components. Prerendering is covered in Prerender and integrate ASP.NET Core
Razor components.

While an app is prerendering, certain actions, such as calling into JavaScript (JS), aren't
possible.

For the following example, the setElementText1 function is placed inside the <head>
element. The function is called with JSRuntimeExtensions.InvokeVoidAsync and doesn't
return a value.

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

HTML

<script>
window.setElementText1 = (element, text) => element.innerText = text;
</script>

2 Warning

The preceding example modifies the Document Object Model (DOM) directly for
demonstration purposes only. Directly modifying the DOM with JS isn't
recommended in most scenarios because JS can interfere with Blazor's change
tracking. For more information, see ASP.NET Core Blazor JavaScript
interoperability (JS interop).

The OnAfterRender{Async} lifecycle event isn't called during the prerendering process
on the server. Override the OnAfterRender{Async} method to delay JS interop calls until
after the component is rendered and interactive on the client after prerendering.

Pages/PrerenderedInterop1.razor :

razor

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
private ElementReference divElement;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync(
"setElementText1", divElement, "Text after render");
}
}
}

7 Note

The preceding example pollutes the client with global methods. For a better
approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

JavaScript

export setElementText1 = (element, text) => element.innerText = text;

The following component demonstrates how to use JS interop as part of a component's


initialization logic in a way that's compatible with prerendering. The component shows
that it's possible to trigger a rendering update from inside OnAfterRenderAsync. The
developer must be careful to avoid creating an infinite loop in this scenario.

For the following example, the setElementText2 function is placed inside the <head>
element. The function is called with IJSRuntime.InvokeAsync and returns a value.

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

HTML

<script>
window.setElementText2 = (element, text) => {
element.innerText = text;
return text;
};
</script>
2 Warning

The preceding example modifies the Document Object Model (DOM) directly for
demonstration purposes only. Directly modifying the DOM with JS isn't
recommended in most scenarios because JS can interfere with Blazor's change
tracking. For more information, see ASP.NET Core Blazor JavaScript
interoperability (JS interop).

Where JSRuntime.InvokeAsync is called, the ElementReference is only used in


OnAfterRenderAsync and not in any earlier lifecycle method because there's no JS
element until after the component is rendered.

StateHasChanged is called to rerender the component with the new state obtained from
the JS interop call (for more information, see ASP.NET Core Razor component
rendering). The code doesn't create an infinite loop because StateHasChanged is only
called when data is null .

Pages/PrerenderedInterop2.razor :

razor

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
Get value via JS interop call:
<strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
private string? data;
private ElementReference divElement;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender && data == null)
{
data = await JS.InvokeAsync<string>(
"setElementText2", divElement, "Hello from interop call!");

StateHasChanged();
}
}
}

7 Note

The preceding example pollutes the client with global methods. For a better
approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

JavaScript

export setElementText2 = (element, text) => {


element.innerText = text;
return text;
};

Component disposal with IDisposable and


IAsyncDisposable
If a component implements IDisposable, IAsyncDisposable, or both, the framework calls
for unmanaged resource disposal when the component is removed from the UI.
Disposal can occur at any time, including during component initialization.

Components shouldn't need to implement IDisposable and IAsyncDisposable


simultaneously. If both are implemented, the framework only executes the asynchronous
overload.

Developer code must ensure that IAsyncDisposable implementations don't take a long
time to complete.

Disposal of JavaScript interop object references


Examples throughout the JavaScript (JS) interop articles demonstrate typical object
disposal patterns:
When calling JS from .NET, as described in Call JavaScript functions from .NET
methods in ASP.NET Core Blazor, dispose any created
IJSObjectReference/IJSInProcessObjectReference/ JSObjectReference either from
.NET or from JS to avoid leaking JS memory.

When calling .NET from JS, as described in Call .NET methods from JavaScript
functions in ASP.NET Core Blazor, dispose of a created DotNetObjectReference
either from .NET or from JS to avoid leaking .NET memory.

JS interop object references are implemented as a map keyed by an identifier on the


side of the JS interop call that creates the reference. When object disposal is initiated
from either the .NET or JS side, Blazor removes the entry from the map, and the object
can be garbage collected as long as no other strong reference to the object is present.

At a minimum, always dispose objects created on the .NET side to avoid leaking .NET
managed memory.

Document Object Model (DOM) cleanup tasks during


component disposal
Don't execute JS interop code for DOM cleanup tasks during component disposal.
Instead, use the MutationObserver pattern in JavaScript on the client for the following
reasons:

The component may have been removed from the DOM by the time your cleanup
code executes in Dispose{Async} .
In a Blazor Server app, the Blazor renderer may have been disposed by the
framework by the time your cleanup code executes in Dispose{Async} .

The MutationObserver pattern allows you to run a function when an element is


removed from the DOM.

For guidance on JSDisconnectedException in Blazor Server apps when a circuit is


disconnected, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor
or Call .NET methods from JavaScript functions in ASP.NET Core Blazor. For general
JavaScript interop error handling guidance, see the JavaScript interop section in Handle
errors in ASP.NET Core Blazor apps.

Synchronous IDisposable
For synchronous disposal tasks, use IDisposable.Dispose.
The following component:

Implements IDisposable with the @implements Razor directive.


Disposes of obj , which is an unmanaged type that implements IDisposable.
A null check is performed because obj is created in a lifecycle method (not
shown).

razor

@implements IDisposable

...

@code {
...

public void Dispose()


{
obj?.Dispose();
}
}

If a single object requires disposal, a lambda can be used to dispose of the object when
Dispose is called. The following example appears in the ASP.NET Core Razor component
rendering article and demonstrates the use of a lambda expression for the disposal of a
Timer.

Pages/CounterWithTimerDisposal1.razor :

razor

@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
private int currentCount = 0;
private Timer timer = new(1000);

protected override void OnInitialized()


{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}

private void OnTimerCallback()


{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}

public void Dispose() => timer.Dispose();


}

7 Note

In the preceding example, the call to StateHasChanged is wrapped by a call to


ComponentBase.InvokeAsync because the callback is invoked outside of Blazor's
synchronization context. For more information, see ASP.NET Core Razor
component rendering.

If the object is created in a lifecycle method, such as OnInitialized/OnInitializedAsync,


check for null before calling Dispose .

Pages/CounterWithTimerDisposal2.razor :

razor

@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
private int currentCount = 0;
private Timer? timer;

protected override void OnInitialized()


{
timer = new Timer(1000);
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}

private void OnTimerCallback()


{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}

public void Dispose() => timer?.Dispose();


}

For more information, see:

Cleaning up unmanaged resources (.NET documentation)


Null-conditional operators ?. and ?[]

Asynchronous IAsyncDisposable
For asynchronous disposal tasks, use IAsyncDisposable.DisposeAsync.

The following component:

Implements IAsyncDisposable with the @implements Razor directive.


Disposes of obj , which is an unmanaged type that implements IAsyncDisposable.
A null check is performed because obj is created in a lifecycle method (not
shown).

razor

@implements IAsyncDisposable

...

@code {
...

public async ValueTask DisposeAsync()


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

For more information, see:

Cleaning up unmanaged resources (.NET documentation)


Null-conditional operators ?. and ?[]

Assignment of null to disposed objects


Usually, there's no need to assign null to disposed objects after calling
Dispose/DisposeAsync. Rare cases for assigning null include the following:

If the object's type is poorly implemented and doesn't tolerate repeat calls to
Dispose/DisposeAsync, assign null after disposal to gracefully skip further calls to
Dispose/DisposeAsync.
If a long-lived process continues to hold a reference to a disposed object,
assigning null allows the garbage collector to free the object in spite of the long-
lived process holding a reference to it.

These are unusual scenarios. For objects that are implemented correctly and behave
normally, there's no point in assigning null to disposed objects. In the rare cases where
an object must be assigned null , we recommend documenting the reason and seeking
a solution that prevents the need to assign null .

StateHasChanged

7 Note

Calling StateHasChanged in Dispose isn't supported. StateHasChanged might be


invoked as part of tearing down the renderer, so requesting UI updates at that
point isn't supported.

Event handlers
Always unsubscribe event handlers from .NET events. The following Blazor form
examples show how to unsubscribe an event handler in the Dispose method:

Private field and lambda approach

razor

@implements IDisposable

<EditForm EditContext="@editContext">
...
<button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
...

private EventHandler<FieldChangedEventArgs>? fieldChanged;


protected override void OnInitialized()
{
editContext = new(model);

fieldChanged = (_, __) =>


{
...
};

editContext.OnFieldChanged += fieldChanged;
}

public void Dispose()


{
editContext.OnFieldChanged -= fieldChanged;
}
}

Private method approach

razor

@implements IDisposable

<EditForm EditContext="@editContext">
...
<button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
...

protected override void OnInitialized()


{
editContext = new(model);
editContext.OnFieldChanged += HandleFieldChanged;
}

private void HandleFieldChanged(object sender,


FieldChangedEventArgs e)
{
...
}

public void Dispose()


{
editContext.OnFieldChanged -= HandleFieldChanged;
}
}
For more information, see the Component disposal with IDisposable and
IAsyncDisposable section.

Anonymous functions, methods, and expressions


When anonymous functions, methods, or expressions, are used, it isn't necessary to
implement IDisposable and unsubscribe delegates. However, failing to unsubscribe a
delegate is a problem when the object exposing the event outlives the lifetime of the
component registering the delegate. When this occurs, a memory leak results because
the registered delegate keeps the original object alive. Therefore, only use the following
approaches when you know that the event delegate disposes quickly. When in doubt
about the lifetime of objects that require disposal, subscribe a delegate method and
properly dispose the delegate as the earlier examples show.

Anonymous lambda method approach (explicit disposal not required):

C#

private void HandleFieldChanged(object sender, FieldChangedEventArgs e)


{
formInvalid = !editContext.Validate();
StateHasChanged();
}

protected override void OnInitialized()


{
editContext = new(starship);
editContext.OnFieldChanged += (s, e) =>
HandleFieldChanged((editContext)s, e);
}

Anonymous lambda expression approach (explicit disposal not required):

C#

private ValidationMessageStore? messageStore;

[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }

protected override void OnInitialized()


{
...

messageStore = new(CurrentEditContext);

CurrentEditContext.OnValidationRequested += (s, e) =>


messageStore.Clear();
CurrentEditContext.OnFieldChanged += (s, e) =>
messageStore.Clear(e.FieldIdentifier);
}

The full example of the preceding code with anonymous lambda expressions
appears in the ASP.NET Core Blazor forms and input components article.

For more information, see Cleaning up unmanaged resources and the topics that follow
it on implementing the Dispose and DisposeAsync methods.

Cancelable background work


Components often perform long-running background work, such as making network
calls (HttpClient) and interacting with databases. It's desirable to stop the background
work to conserve system resources in several situations. For example, background
asynchronous operations don't automatically stop when a user navigates away from a
component.

Other reasons why background work items might require cancellation include:

An executing background task was started with faulty input data or processing
parameters.
The current set of executing background work items must be replaced with a new
set of work items.
The priority of currently executing tasks must be changed.
The app must be shut down for server redeployment.
Server resources become limited, necessitating the rescheduling of background
work items.

To implement a cancelable background work pattern in a component:

Use a CancellationTokenSource and CancellationToken.


On disposal of the component and at any point cancellation is desired by manually
canceling the token, call CancellationTokenSource.Cancel to signal that the
background work should be cancelled.
After the asynchronous call returns, call ThrowIfCancellationRequested on the
token.

In the following example:

await Task.Delay(5000, cts.Token); represents long-running asynchronous


background work.
BackgroundResourceMethod represents a long-running background method that

shouldn't start if the Resource is disposed before the method is called.

Pages/BackgroundWork.razor :

razor

@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>


<button @onclick="Dispose">Trigger Disposal</button>

@code {
private Resource resource = new();
private CancellationTokenSource cts = new();

protected async Task LongRunningWork()


{
Logger.LogInformation("Long running work started");

await Task.Delay(5000, cts.Token);

cts.Token.ThrowIfCancellationRequested();
resource.BackgroundResourceMethod(Logger);
}

public void Dispose()


{
Logger.LogInformation("Executing Dispose");
cts.Cancel();
cts.Dispose();
resource?.Dispose();
}

private class Resource : IDisposable


{
private bool disposed;

public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)


{
logger.LogInformation("BackgroundResourceMethod: Start method");

if (disposed)
{
logger.LogInformation("BackgroundResourceMethod: Disposed");
throw new ObjectDisposedException(nameof(Resource));
}

// Take action on the Resource


logger.LogInformation("BackgroundResourceMethod: Action on
Resource");
}

public void Dispose()


{
disposed = true;
}
}
}

Blazor Server reconnection events


The component lifecycle events covered in this article operate separately from Blazor
Server's reconnection event handlers. When a Blazor Server app loses its SignalR
connection to the client, only UI updates are interrupted. UI updates are resumed when
the connection is re-established. For more information on circuit handler events and
configuration, see ASP.NET Core Blazor SignalR guidance.
ASP.NET Core Razor component
virtualization
Article • 05/05/2023

This article explains how to use component virtualization in ASP.NET Core Blazor apps.

Improve the perceived performance of component rendering using the Blazor


framework's built-in virtualization support with the Virtualize component. Virtualization
is a technique for limiting UI rendering to just the parts that are currently visible. For
example, virtualization is helpful when the app must render a long list of items and only
a subset of items is required to be visible at any given time.

Use the Virtualize component (reference source) when:

Rendering a set of data items in a loop.


Most of the items aren't visible due to scrolling.
The rendered items are the same size.

When the user scrolls to an arbitrary point in the Virtualize component's list of items,
the component calculates the visible items to show. Unseen items aren't rendered.

Without virtualization, a typical list might use a C# foreach loop to render each item in a
list. In the following example:

allFlights is a collection of airplane flights.

The FlightSummary component displays details about each flight.


The @key directive attribute preserves the relationship of each FlightSummary
component to its rendered flight by the flight's FlightId .

razor

<div style="height:500px;overflow-y:scroll">
@foreach (var flight in allFlights)
{
<FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
}
</div>

If the collection contains thousands of flights, rendering the flights takes a long time
and users experience a noticeable UI lag. Most of the flights aren't seen because they
fall outside of the height of the <div> element.
Instead of rendering the entire list of flights at once, replace the foreach loop in the
preceding example with the Virtualize component:

Specify allFlights as a fixed item source to Virtualize<TItem>.Items. Only the


currently visible flights are rendered by the Virtualize component.
Specify a context for each flight with the Context parameter. In the following
example, flight is used as the context, which provides access to each flight's
members.

razor

<div style="height:500px;overflow-y:scroll">
<Virtualize Items="@allFlights" Context="flight">
<FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
</Virtualize>
</div>

If a context isn't specified with the Context parameter, use the value of context in the
item content template to access each flight's members:

razor

<div style="height:500px;overflow-y:scroll">
<Virtualize Items="@allFlights">
<FlightSummary @key="context.FlightId" Details="@context.Summary" />
</Virtualize>
</div>

The Virtualize component:

Calculates the number of items to render based on the height of the container and
the size of the rendered items.
Recalculates and rerenders the items as the user scrolls.
Only fetches the slice of records from an external API that correspond to the
current visible region, instead of downloading all of the data from the collection.
Receives a generic ICollection<T> for Virtualize<TItem>.Items. If a non-generic
collection supplies the items (for example, a collection of DataRow), follow the
guidance in the Item provider delegate section to supply the items.

The item content for the Virtualize component can include:

Plain HTML and Razor code, as the preceding example shows.


One or more Razor components.
A mix of HTML/Razor and Razor components.
Item provider delegate
If you don't want to load all of the items into memory or the collection isn't a generic
ICollection<T>, you can specify an items provider delegate method to the component's
Virtualize<TItem>.ItemsProvider parameter that asynchronously retrieves the requested
items on demand. In the following example, the LoadEmployees method provides the
items to the Virtualize component:

razor

<Virtualize Context="employee" ItemsProvider="@LoadEmployees">


<p>
@employee.FirstName @employee.LastName has the
job title of @employee.JobTitle.
</p>
</Virtualize>

The items provider receives an ItemsProviderRequest, which specifies the required


number of items starting at a specific start index. The items provider then retrieves the
requested items from a database or other service and returns them as an
ItemsProviderResult<TItem> along with a count of the total items. The items provider
can choose to retrieve the items with each request or cache them so that they're readily
available.

A Virtualize component can only accept one item source from its parameters, so don't
attempt to simultaneously use an items provider and assign a collection to Items . If
both are assigned, an InvalidOperationException is thrown when the component's
parameters are set at runtime.

The following example loads employees from an EmployeeService (not shown):

C#

private async ValueTask<ItemsProviderResult<Employee>> LoadEmployees(


ItemsProviderRequest request)
{
var numEmployees = Math.Min(request.Count, totalEmployees -
request.StartIndex);
var employees = await
EmployeesService.GetEmployeesAsync(request.StartIndex,
numEmployees, request.CancellationToken);

return new ItemsProviderResult<Employee>(employees, totalEmployees);


}
In the following example, a collection of DataRow is a non-generic collection, so an
items provider delegate is used for virtualization:

razor

<Virtualize Context="row" ItemsProvider="@GetRows">


...
</Virtualize>

@code{
...

private ValueTask<ItemsProviderResult<DataRow>>
GetRows(ItemsProviderRequest request)
{
return new(new ItemsProviderResult<DataRow>(
dataTable.Rows.OfType<DataRow>
().Skip(request.StartIndex).Take(request.Count),
dataTable.Rows.Count));
}
}

Virtualize<TItem>.RefreshDataAsync instructs the component to rerequest data from its


ItemsProvider. This is useful when external data changes. There's usually no need to call
RefreshDataAsync when using Items.

RefreshDataAsync updates a Virtualize component's data without causing a rerender.


If RefreshDataAsync is invoked from a Blazor event handler or component lifecycle
method, triggering a render isn't required because a render is automatically triggered at
the end of the event handler or lifecycle method. If RefreshDataAsync is triggered
separately from a background task or event, such as in the following ForecastUpdated
delegate, call StateHasChanged to update the UI at the end of the background task or
event:

C#

<Virtualize ... @ref="virtualizeComponent">


...
</Virtualize>

...

private Virtualize<FetchData>? virtualizeComponent;

protected override void OnInitialized()


{
WeatherForecastSource.ForecastUpdated += async () =>
{
await InvokeAsync(async () =>
{
await virtualizeComponent?.RefreshDataAsync();
StateHasChanged();
});
});
}

In the preceding example:

RefreshDataAsync is called first to obtain new data for the Virtualize component.
StateHasChanged is called to rerender the component.

Placeholder
Because requesting items from a remote data source might take some time, you have
the option to render a placeholder with item content:

Use a Placeholder ( <Placeholder>...</Placeholder> ) to display content until the


item data is available.
Use Virtualize<TItem>.ItemContent to set the item template for the list.

razor

<Virtualize Context="employee" ItemsProvider="@LoadEmployees">


<ItemContent>
<p>
@employee.FirstName @employee.LastName has the
job title of @employee.JobTitle.
</p>
</ItemContent>
<Placeholder>
<p>
Loading&hellip;
</p>
</Placeholder>
</Virtualize>

Item size
The height of each item in pixels can be set with Virtualize<TItem>.ItemSize (default:
50). The following example changes the height of each item from the default of 50 pixels
to 25 pixels:

razor
<Virtualize Context="employee" Items="@employees" ItemSize="25">
...
</Virtualize>

By default, the Virtualize component measures the rendering size (height) of


individual items after the initial render occurs. Use ItemSize to provide an exact item size
in advance to assist with accurate initial render performance and to ensure the correct
scroll position for page reloads. If the default ItemSize causes some items to render
outside of the currently visible view, a second re-render is triggered. To correctly
maintain the browser's scroll position in a virtualized list, the initial render must be
correct. If not, users might view the wrong items.

Overscan count
Virtualize<TItem>.OverscanCount determines how many additional items are rendered
before and after the visible region. This setting helps to reduce the frequency of
rendering during scrolling. However, higher values result in more elements rendered in
the page (default: 3). The following example changes the overscan count from the
default of three items to four items:

razor

<Virtualize Context="employee" Items="@employees" OverscanCount="4">


...
</Virtualize>

State changes
When making changes to items rendered by the Virtualize component, call
StateHasChanged to force re-evaluation and rerendering of the component. For more
information, see ASP.NET Core Razor component rendering.

Keyboard scroll support


To allow users to scroll virtualized content using their keyboard, ensure that the
virtualized elements or scroll container itself is focusable. If you fail to take this step,
keyboard scrolling doesn't work in Chromium-based browsers.

For example, you can use a tabindex attribute on the scroll container:
razor

<div style="height:500px; overflow-y:scroll" tabindex="-1">


<Virtualize Items="@allFlights">
<div class="flight-info">...</div>
</Virtualize>
</div>

To learn more about the meaning of tabindex value -1 , 0 , or other values, see tabindex
(MDN documentation) .

Advanced styles and scroll detection


The Virtualize component is only designed to support specific element layout
mechanisms. To understand which element layouts work correctly, the following explains
how Virtualize detects which elements should be visible for display in the correct
place.

If your source code looks like the following:

razor

<div style="height:500px; overflow-y:scroll" tabindex="-1">


<Virtualize Items="@allFlights" ItemSize="100">
<div class="flight-info">Flight @context.Id</div>
</Virtualize>
</div>

At runtime, the Virtualize component renders a DOM structure similar to the


following:

HTML

<div style="height:500px; overflow-y:scroll" tabindex="-1">


<div style="height:1100px"></div>
<div class="flight-info">Flight 12</div>
<div class="flight-info">Flight 13</div>
<div class="flight-info">Flight 14</div>
<div class="flight-info">Flight 15</div>
<div class="flight-info">Flight 16</div>
<div style="height:3400px"></div>
</div>

The actual number of rows rendered and the size of the spacers vary according to your
styling and Items collection size. However, notice that there are spacer div elements
injected before and after your content. These serve two purposes:

To provide an offset before and after your content, causing currently-visible items
to appear at the correct location in the scroll range and the scroll range itself to
represent the total size of all content.
To detect when the user is scrolling beyond the current visible range, meaning that
different content must be rendered.

7 Note

To learn how to control the spacer HTML element tag, see the Control the spacer
element tag name section later in this article.

The spacer elements internally use an Intersection Observer to receive notification


when they're becoming visible. Virtualize depends on receiving these events.
Virtualize works under the following conditions:

All content items are of identical height. This makes it possible to calculate which
content corresponds to a given scroll position without first fetching every data
item and rendering the data into a DOM element.

Both the spacers and the content rows are rendered in a single vertical stack
with every item filling the whole horizontal width. This is generally the default. In
typical cases with div elements, Virtualize works by default. If you're using CSS
to create a more advanced layout, bear in mind the following requirements:
Scroll container styling requires a display with any of the following values:
block (the default for a div ).
table-row-group (the default for a tbody ).

flex with flex-direction set to column . Ensure that immediate children of

the Virtualize component don't shrink under flex rules. For example, add
.mycontainer > div { flex-shrink: 0 } .

Content row styling requires a display with either of the following values:
block (the default for a div ).

table-row (the default for a tr ).

Don't use CSS to interfere with the layout for the spacer elements. By default,
the spacer elements have a display value of block , except if the parent is a
table row group, in which case they default to table-row . Don't try to influence
spacer element width or height, including by causing them to have a border or
content pseudo-elements.
Any approach that stops the spacers and content elements from rendering as a single
vertical stack, or causes the content items to vary in height, prevents correct functioning
of the Virtualize component.

Root-level virtualization
The Virtualize component supports using the document itself as the scroll root, as an
alternative to having some other element with overflow-y: scroll . In the following
example, the <html> or <body> elements are styled in a component with overflow-y:
scroll :

razor

<HeadContent>
<style>
html, body { overflow-y: scroll }
</style>
</HeadContent>

Control the spacer element tag name


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.
The default value is div . For the following example, the Virtualize component renders
inside a table body element (tbody ), so the appropriate child element for a table row
(tr ) is set as the spacer.

Pages/VirtualizedTable.razor :

razor

@page "/virtualized-table"

<HeadContent>
<style>
html, body { overflow-y: scroll }
</style>
</HeadContent>

<h1>Virtualized Table Example</h1>

<table id="virtualized-table">
<thead style="position: sticky; top: 0; background-color: silver">
<tr>
<th>Item</th>
<th>Another column</th>
</tr>
</thead>
<tbody>
<Virtualize Items="@fixedItems" ItemSize="30" SpacerElement="tr">
<tr @key="context" style="height: 30px;" id="row-@context">
<td>Item @context</td>
<td>Another value</td>
</tr>
</Virtualize>
</tbody>
</table>

@code {
private List<int> fixedItems = Enumerable.Range(0, 1000).ToList();
}

In the preceding example, the document root is used as the scroll container, so the html
and body elements are styled with overflow-y: scroll . For more information, see the
following resources:

Root-level virtualization section


Control head content in ASP.NET Core Blazor apps
ASP.NET Core Razor component
rendering
Article • 04/04/2023

This article explains Razor component rendering in ASP.NET Core Blazor apps, including
when to call StateHasChanged to manually trigger a component to render.

Components must render when they're first added to the component hierarchy by a
parent component. This is the only time that a component must render. Components
may render at other times according to their own logic and conventions.

Rendering conventions for ComponentBase


By default, Razor components inherit from the ComponentBase base class, which
contains logic to trigger rerendering at the following times:

After applying an updated set of parameters from a parent component.


After applying an updated value for a cascading parameter.
After notification of an event and invoking one of its own event handlers.
After a call to its own StateHasChanged method (see ASP.NET Core Razor
component lifecycle). For guidance on how to prevent overwriting child
component parameters when StateHasChanged is called in a parent component,
see Avoid overwriting parameters in ASP.NET Core Blazor.

Components inherited from ComponentBase skip rerenders due to parameter updates if


either of the following are true:

All of the parameters are from a set of known types† or any primitive type that
hasn't changed since the previous set of parameters were set.

†The Blazor framework uses a set of built-in rules and explicit parameter type
checks for change detection. These rules and the types are subject to change at
any time. For more information, see the ChangeDetection API in the ASP.NET Core
reference source .

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

The component's ShouldRender method returns false .

Control the rendering flow


In most cases, ComponentBase conventions result in the correct subset of component
rerenders after an event occurs. Developers aren't usually required to provide manual
logic to tell the framework which components to rerender and when to rerender them.
The overall effect of the framework's conventions is that the component receiving an
event rerenders itself, which recursively triggers rerendering of descendant components
whose parameter values may have changed.

For more information on the performance implications of the framework's conventions


and how to optimize an app's component hierarchy for rendering, see ASP.NET Core
Blazor performance best practices.

Suppress UI refreshing ( ShouldRender )


ShouldRender is called each time a component is rendered. Override ShouldRender to
manage UI refreshing. If the implementation returns true , the UI is refreshed.

Even if ShouldRender is overridden, the component is always initially rendered.

Pages/ControlRender.razor :

razor

@page "/control-render"

<label>
<input type="checkbox" @bind="shouldRender" />
Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
<button @onclick="IncrementCount">Click me</button>
</p>

@code {
private int currentCount = 0;
private bool shouldRender = true;
protected override bool ShouldRender()
{
return shouldRender;
}

private void IncrementCount()


{
currentCount++;
}
}

For more information on performance best practices pertaining to ShouldRender, see


ASP.NET Core Blazor performance best practices.

When to call StateHasChanged


Calling StateHasChanged allows you to trigger a render at any time. However, be careful
not to call StateHasChanged unnecessarily, which is a common mistake that imposes
unnecessary rendering costs.

Code shouldn't need to call StateHasChanged when:

Routinely handling events, whether synchronously or asynchronously, since


ComponentBase triggers a render for most routine event handlers.
Implementing typical lifecycle logic, such as OnInitialized or
OnParametersSetAsync, whether synchronously or asynchronously, since
ComponentBase triggers a render for typical lifecycle events.

However, it might make sense to call StateHasChanged in the cases described in the
following sections of this article:

An asynchronous handler involves multiple asynchronous phases


Receiving a call from something external to the Blazor rendering and event
handling system
To render component outside the subtree that is rerendered by a particular event

An asynchronous handler involves multiple asynchronous


phases
Due to the way that tasks are defined in .NET, a receiver of a Task can only observe its
final completion, not intermediate asynchronous states. Therefore, ComponentBase can
only trigger rerendering when the Task is first returned and when the Task finally
completes. The framework can't know to rerender a component at other intermediate
points, such as when an IAsyncEnumerable<T> returns data in a series of intermediate
Tasks . If you want to rerender at intermediate points, call StateHasChanged at those
points.

Consider the following CounterState1 component, which updates the count four times
on each click:

Automatic renders occur after the first and last increments of currentCount .
Manual renders are triggered by calls to StateHasChanged when the framework
doesn't automatically trigger rerenders at intermediate processing points where
currentCount is incremented.

Pages/CounterState1.razor :

razor

@page "/counter-state-1"

<p>
Current count: @currentCount
</p>

<p>
<button class="btn btn-primary" @onclick="IncrementCount">Click
me</button>
</p>

@code {
private int currentCount = 0;

private async Task IncrementCount()


{
currentCount++;
// Renders here automatically

await Task.Delay(1000);
currentCount++;
StateHasChanged();

await Task.Delay(1000);
currentCount++;
StateHasChanged();

await Task.Delay(1000);
currentCount++;
// Renders here automatically
}
}
Receiving a call from something external to the Blazor
rendering and event handling system
ComponentBase only knows about its own lifecycle methods and Blazor-triggered
events. ComponentBase doesn't know about other events that may occur in code. For
example, any C# events raised by a custom data store are unknown to Blazor. In order
for such events to trigger rerendering to display updated values in the UI, call
StateHasChanged.

Consider the following CounterState2 component that uses System.Timers.Timer to


update a count at a regular interval and calls StateHasChanged to update the UI:

OnTimerCallback runs outside of any Blazor-managed rendering flow or event

notification. Therefore, OnTimerCallback must call StateHasChanged because


Blazor isn't aware of the changes to currentCount in the callback.
The component implements IDisposable, where the Timer is disposed when the
framework calls the Dispose method. For more information, see ASP.NET Core
Razor component lifecycle.

Because the callback is invoked outside of Blazor's synchronization context, the


component must wrap the logic of OnTimerCallback in ComponentBase.InvokeAsync to
move it onto the renderer's synchronization context. This is equivalent to marshalling to
the UI thread in other UI frameworks. StateHasChanged can only be called from the
renderer's synchronization context and throws an exception otherwise:

System.InvalidOperationException: 'The current thread is not associated with the


Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering
rendering or component state.'

Pages/CounterState2.razor :

razor

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
Current count: @currentCount
</p>

@code {
private int currentCount = 0;
private Timer timer = new(1000);

protected override void OnInitialized()


{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}

private void OnTimerCallback()


{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}

public void Dispose() => timer.Dispose();


}

To render a component outside the subtree that's


rerendered by a particular event
The UI might involve:

1. Dispatching an event to one component.


2. Changing some state.
3. Rerendering a completely different component that isn't a descendant of the
component receiving the event.

One way to deal with this scenario is to provide a state management class, often as a
dependency injection (DI) service, injected into multiple components. When one
component calls a method on the state manager, the state manager raises a C# event
that's then received by an independent component.

For approaches to manage state, see the following resources:

In-memory state container service (Blazor Server) (Blazor WebAssembly equivalent)


section of the State management article.
Pass data across a component hierarchy using cascading values and parameters.
Bind across more than two components using data bindings.

For the state manager approach, C# events are outside the Blazor rendering pipeline.
Call StateHasChanged on other components you wish to rerender in response to the
state manager's events.
The state manager approach is similar to the earlier case with System.Timers.Timer in the
previous section. Since the execution call stack typically remains on the renderer's
synchronization context, calling InvokeAsync isn't normally required. Calling
InvokeAsync is only required if the logic escapes the synchronization context, such as
calling ContinueWith on a Task or awaiting a Task with ConfigureAwait(false). For more
information, see the Receiving a call from something external to the Blazor rendering
and event handling system section.
ASP.NET Core Blazor templated
components
Article • 05/30/2023

Templated components are components that receive one or more UI templates as


parameters, which can be utilized in the rendering logic of the component. By using
templated components, you can create higher-level components that are more reusable.
A couple of examples include:

A table component that allows a user to specify templates for the table's header,
rows, and footer.
A list component that allows a user to specify a template for rendering items in a
list.

A templated component is defined by specifying one or more component parameters of


type RenderFragment or RenderFragment<TValue>. A render fragment represents a
segment of UI to render. RenderFragment<TValue> takes a type parameter that can be
specified when the render fragment is invoked.

7 Note

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

Often, templated components are generically typed, as the following TableTemplate


component demonstrates. The generic type <T> in this example is used to render
IReadOnlyList<T> values, which in this case is a series of pet rows in a component that

displays a table of pets.

Shared/TableTemplate.razor :

razor

@typeparam TItem
@using System.Diagnostics.CodeAnalysis

<table class="table">
<thead>
<tr>@TableHeader</tr>
</thead>
<tbody>
@foreach (var item in Items)
{
if (RowTemplate is not null)
{
<tr>@RowTemplate(item)</tr>
}
}
</tbody>
</table>

@code {
[Parameter]
public RenderFragment? TableHeader { get; set; }

[Parameter]
public RenderFragment<TItem>? RowTemplate { get; set; }

[Parameter, AllowNull]
public IReadOnlyList<TItem> Items { get; set; }
}

When using a templated component, the template parameters can be specified using
child elements that match the names of the parameters. In the following example,
<TableHeader>...</TableHeader> and <RowTemplate>...<RowTemplate> supply

RenderFragment<TValue> templates for TableHeader and RowTemplate of the


TableTemplate component.

Specify the Context attribute on the component element when you want to specify the
content parameter name for implicit child content (without any wrapping child element).
In the following example, the Context attribute appears on the TableTemplate element
and applies to all RenderFragment<TValue> template parameters.

Pages/Pets1.razor :

razor

@page "/pets1"

<h1>Pets</h1>

<TableTemplate Items="pets" Context="pet">


<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@pet.PetId</td>
<td>@pet.Name</td>
</RowTemplate>
</TableTemplate>

@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};

private class Pet


{
public int PetId { get; set; }
public string? Name { get; set; }
}
}

Alternatively, you can change the parameter name using the Context attribute on the
RenderFragment<TValue> child element. In the following example, the Context is set on
RowTemplate rather than TableTemplate :

Pages/Pets2.razor :

razor

@page "/pets2"

<h1>Pets</h1>

<TableTemplate Items="pets">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate Context="pet">
<td>@pet.PetId</td>
<td>@pet.Name</td>
</RowTemplate>
</TableTemplate>

@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};

private class Pet


{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
Component arguments of type RenderFragment<TValue> have an implicit parameter
named context , which can be used. In the following example, Context isn't set.
@context.{PROPERTY} supplies pet values to the template, where {PROPERTY} is a Pet

property:

Pages/Pets3.razor :

razor

@page "/pets3"

<h1>Pets</h1>

<TableTemplate Items="pets">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@context.PetId</td>
<td>@context.Name</td>
</RowTemplate>
</TableTemplate>

@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};

private class Pet


{
public int PetId { get; set; }
public string? Name { get; set; }
}
}

When using generic-typed components, the type parameter is inferred if possible.


However, you can explicitly specify the type with an attribute that has a name matching
the type parameter, which is TItem in the preceding example:

Pages/Pets4.razor :

razor

@page "/pets4"

<h1>Pets</h1>
<TableTemplate Items="pets" TItem="Pet">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@context.PetId</td>
<td>@context.Name</td>
</RowTemplate>
</TableTemplate>

@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};

private class Pet


{
public int PetId { get; set; }
public string? Name { get; set; }
}
}

Additional resources
ASP.NET Core Blazor performance best practices
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor CSS isolation
Article • 05/05/2023

By Dave Brock

This article explains how CSS isolation scopes CSS to Razor components, which can
simplify CSS and avoid collisions with other components or libraries.

Isolate CSS styles to individual pages, views, and components to reduce or avoid:

Dependencies on global styles that can be challenging to maintain.


Style conflicts in nested content.

Enable CSS isolation


To define component-specific styles, create a .razor.css file matching the name of the
.razor file for the component in the same folder. The .razor.css file is a scoped CSS

file.

For an Example component in an Example.razor file, create a file alongside the


component named Example.razor.css . The Example.razor.css file must reside in the
same folder as the Example component ( Example.razor ). The " Example " base name of
the file is not case-sensitive.

Pages/Example.razor :

razor

@page "/example"

<h1>Scoped CSS Example</h1>

Pages/Example.razor.css :

css

h1 {
color: brown;
font-family: Tahoma, Geneva, Verdana, sans-serif;
}
The styles defined in Example.razor.css are only applied to the rendered output of
the Example component. CSS isolation is applied to HTML elements in the matching
Razor file. Any h1 CSS declarations defined elsewhere in the app don't conflict with the
Example component's styles.

7 Note

In order to guarantee style isolation when bundling occurs, importing CSS in Razor
code blocks isn't supported.

CSS isolation bundling


CSS isolation occurs at build time. Blazor rewrites CSS selectors to match markup
rendered by the component. The rewritten CSS styles are bundled and produced as a
static asset. The stylesheet is referenced inside the <head> tag (location of <head>
content). The following <link> element is added by default to an app created from the
Blazor project templates, where the placeholder {ASSEMBLY NAME} is the project's
assembly name:

HTML

<link href="{ASSEMBLY NAME}.styles.css" rel="stylesheet">

The following example is from a hosted Blazor WebAssembly Client app. The app's
assembly name is BlazorSample.Client , and the <link> is added by the Blazor
WebAssembly project template when the project is created with the Hosted option ( -
ho|--hosted option using the .NET CLI or ASP.NET Core Hosted checkbox using Visual

Studio):

HTML

<link href="BlazorSample.Client.styles.css" rel="stylesheet">

Within the bundled file, each component is associated with a scope identifier. For each
styled component, an HTML attribute is appended with the format b-{STRING} , where
the {STRING} placeholder is a ten-character string generated by the framework. The
identifier is unique for each app. In the rendered Counter component, Blazor appends a
scope identifier to the h1 element:

HTML
<h1 b-3xxtam6d07>

The {ASSEMBLY NAME}.styles.css file uses the scope identifier to group a style
declaration with its component. The following example provides the style for the
preceding <h1> element:

css

/* /Pages/Counter.razor.rz.scp.css */
h1[b-3xxtam6d07] {
color: brown;
}

At build time, a project bundle is created with the convention


obj/{CONFIGURATION}/{TARGET FRAMEWORK}/scopedcss/projectbundle/{ASSEMBLY

NAME}.bundle.scp.css , where the placeholders are:

{CONFIGURATION} : The app's build configuration (for example, Debug , Release ).


{TARGET FRAMEWORK} : The target framework (for example, net6.0 ).

{ASSEMBLY NAME} : The app's assembly name (for example, BlazorSample ).

Child component support


By default, CSS isolation only applies to the component you associate with the format
{COMPONENT NAME}.razor.css , where the placeholder {COMPONENT NAME} is usually the

component name. To apply changes to a child component, use the ::deep pseudo-
element to any descendant elements in the parent component's .razor.css file. The
::deep pseudo-element selects elements that are descendants of an element's

generated scope identifier.

The following example shows a parent component called Parent with a child
component called Child .

Pages/Parent.razor :

razor

@page "/parent"

<div>
<h1>Parent component</h1>
<Child />
</div>

Shared/Child.razor :

razor

<h1>Child Component</h1>

Update the h1 declaration in Parent.razor.css with the ::deep pseudo-element to


signify the h1 style declaration must apply to the parent component and its children.

Pages/Parent.razor.css :

css

::deep h1 {
color: red;
}

The h1 style now applies to the Parent and Child components without the need to
create a separate scoped CSS file for the child component.

The ::deep pseudo-element only works with descendant elements. The following
markup applies the h1 styles to components as expected. The parent component's
scope identifier is applied to the div element, so the browser knows to inherit styles
from the parent component.

Pages/Parent.razor :

razor

<div>
<h1>Parent</h1>

<Child />
</div>

However, excluding the div element removes the descendant relationship. In the
following example, the style is not applied to the child component.

Pages/Parent.razor :

razor
<h1>Parent</h1>

<Child />

The ::deep pseudo-element affects where the scope attribute is applied to the rule.
When you define a CSS rule in a scoped CSS file, the scope is applied to the right most
element by default. For example: div > a is transformed to div > a[b-{STRING}] , where
the {STRING} placeholder is a ten-character string generated by the framework (for
example, b-3xxtam6d07 ). If you instead want the rule to apply to a different selector, the
::deep pseudo-element allows you do so. For example, div ::deep > a is transformed

to div[b-{STRING}] > a (for example, div[b-3xxtam6d07] > a ).

The ability to attach the ::deep pseudo-element to any HTML element allows you to
create scoped CSS styles that affect elements rendered by other components when you
can determine the structure of the rendered HTML tags. For a component that renders
an hyperlink tag ( <a> ) inside another component, ensure the component is wrapped in
a div (or any other element) and use the rule ::deep > a to create a style that's only
applied to that component when the parent component renders.

) Important

Scoped CSS only applies to HTML elements and not to Razor components or Tag
Helpers, including elements with a Tag Helper applied, such as <input asp-
for="..." /> .

CSS preprocessor support


CSS preprocessors are useful for improving CSS development by utilizing features such
as variables, nesting, modules, mixins, and inheritance. While CSS isolation doesn't
natively support CSS preprocessors such as Sass or Less, integrating CSS preprocessors
is seamless as long as preprocessor compilation occurs before Blazor rewrites the CSS
selectors during the build process. Using Visual Studio for example, configure existing
preprocessor compilation as a Before Build task in the Visual Studio Task Runner
Explorer.

Many third-party NuGet packages, such as AspNetCore.SassCompiler , can compile


SASS/SCSS files at the beginning of the build process before CSS isolation occurs.

CSS isolation configuration


CSS isolation is designed to work out-of-the-box but provides configuration for some
advanced scenarios, such as when there are dependencies on existing tools or
workflows.

Customize scope identifier format


By default, scope identifiers use the format b-{STRING} , where the {STRING} placeholder
is a ten-character string generated by the framework. To customize the scope identifier
format, update the project file to a desired pattern:

XML

<ItemGroup>
<None Update="Pages/Example.razor.css" CssScope="custom-scope-identifier"
/>
</ItemGroup>

In the preceding example, the CSS generated for Example.razor.css changes its scope
identifier from b-{STRING} to custom-scope-identifier .

Use scope identifiers to achieve inheritance with scoped CSS files. In the following
project file example, a BaseComponent.razor.css file contains common styles across
components. A DerivedComponent.razor.css file inherits these styles.

XML

<ItemGroup>
<None Update="Pages/BaseComponent.razor.css" CssScope="custom-scope-
identifier" />
<None Update="Pages/DerivedComponent.razor.css" CssScope="custom-scope-
identifier" />
</ItemGroup>

Use the wildcard ( * ) operator to share scope identifiers across multiple files:

XML

<ItemGroup>
<None Update="Pages/*.razor.css" CssScope="custom-scope-identifier" />
</ItemGroup>

Change base path for static web assets


The scoped.styles.css file is generated at the root of the app. In the project file, use the
<StaticWebAssetBasePath> property to change the default path. The following example
places the scoped.styles.css file, and the rest of the app's assets, at the _content path:

XML

<PropertyGroup>
<StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>

Disable automatic bundling


To opt out of how Blazor publishes and loads scoped files at runtime, use the
DisableScopedCssBundling property. When using this property, it means other tools or

processes are responsible for taking the isolated CSS files from the obj directory and
publishing and loading them at runtime:

XML

<PropertyGroup>
<DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>

Disable CSS isolation


Disable CSS isolation for a project by setting the <ScopedCssEnabled> property to false
in the app's project file:

XML

<ScopedCssEnabled>false</ScopedCssEnabled>

Razor class library (RCL) support


Isolated styles for components in a NuGet package or Razor class library (RCL) are
automatically bundled:

The app uses CSS imports to reference the RCL's bundled styles. For a class library
named ClassLib and a Blazor app with a BlazorSample.styles.css stylesheet, the
RCL's stylesheet is imported at the top of the app's stylesheet:
css

@import '_content/ClassLib/ClassLib.bundle.scp.css';

The RCL's bundled styles aren't published as a static web asset of the app that
consumes the styles.

For more information on RCLs, see the following articles:

Consume ASP.NET Core Razor components from a Razor class library (RCL)
Reusable Razor UI in class libraries with ASP.NET Core

Additional resources
Razor Pages CSS isolation
MVC CSS isolation
Dynamically-rendered ASP.NET Core
Razor components
Article • 04/04/2023

By Dave Brock

Use the built-in DynamicComponent component to render components by type.

A DynamicComponent is useful for rendering components without iterating through


possible types or using conditional logic. For example, DynamicComponent can render a
component based on a user selection from a dropdown list.

In the following example:

componentType specifies the type.

parameters specifies component parameters to pass to the componentType

component.

razor

<DynamicComponent Type="@componentType" Parameters="@parameters" />

@code {
private Type componentType = ...;
private IDictionary<string, object> parameters = ...;
}

For more information on passing parameter values, see the Pass parameters section
later in this article.

Use the Instance property to access the dynamically-created component instance:

razor

<DynamicComponent Type="@typeof({COMPONENT})" @ref="dc" />

<button @onclick="Refresh">Refresh</button>

@code {
private DynamicComponent? dc;

private Task Refresh()


{
return (dc?.Instance as IRefreshable)?.Refresh();
}
}
In the preceding example:

The {COMPONENT} placeholder is the dynamically-created component type.


IRefreshable is an example interface provided by the developer for the dynamic
component instance.

Example
In the following example, a Razor component renders a component based on the user's
selection from a dropdown list of four possible values.

User spaceflight carrier selection Shared Razor component to render

Rocket Lab® Shared/RocketLab.razor

SpaceX® Shared/SpaceX.razor

ULA® Shared/UnitedLaunchAlliance.razor

Virgin Galactic® Shared/VirginGalactic.razor

Shared/RocketLab.razor :

razor

<h2>Rocket Lab®</h2>

<p>
Rocket Lab is a registered trademark of
<a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

Shared/SpaceX.razor :

razor

<h2>SpaceX®</h2>

<p>
SpaceX is a registered trademark of
<a href="https://www.spacex.com/">Space Exploration Technologies Corp.
</a>
</p>

Shared/UnitedLaunchAlliance.razor :
razor

<h2>United Launch Alliance®</h2>

<p>
United Launch Alliance and ULA are registered trademarks of
<a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>

Shared/VirginGalactic.razor :

razor

<h2>Virgin Galactic®</h2>

<p>
Virgin Galactic is a registered trademark of
<a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>

Pages/DynamicComponentExample1.razor :

razor

@page "/dynamiccomponent-example-1"

<h1><code>DynamicComponent</code> Component Example 1</h1>

<p>
<label>
Select your transport:
<select @onchange="OnDropdownChange">
<option value="">Select a value</option>
<option value="@nameof(RocketLab)">Rocket Lab</option>
<option value="@nameof(SpaceX)">SpaceX</option>
<option value="@nameof(UnitedLaunchAlliance)">ULA</option>
<option value="@nameof(VirginGalactic)">Virgin Galactic</option>
</select>
</label>
</p>

@if (selectedType is not null)


{
<div class="border border-primary my-1 p-1">
<DynamicComponent Type="@selectedType" />
</div>
}

@code {
private Type? selectedType;
private void OnDropdownChange(ChangeEventArgs e)
{
selectedType = e.Value?.ToString()?.Length > 0 ?
Type.GetType($"BlazorSample.Shared.{e.Value}") : null;
}
}

In the preceding example:

Component names are used as the option values using the nameof operator,
which returns component names as constant strings.
The namespace of the app is BlazorSample . Change the namespace to match your
app's namespace.

Pass parameters
If dynamically-rendered components have component parameters, pass them into the
DynamicComponent as an IDictionary<string, object> . The string is the name of the
parameter, and the object is the parameter's value.

The following example configures a component metadata object ( ComponentMetadata ) to


supply parameter values to dynamically-rendered components based on the type name.
The example is just one of several approaches that you can adopt. Parameter data can
also be provided from a web API, a database, or a method. The only requirement is that
the approach returns an IDictionary<string, object> .

ComponentMetadata.cs :

C#

public class ComponentMetadata


{
public string? Name { get; set; }
public Dictionary<string, object> Parameters { get; set; } =
new Dictionary<string, object>();
}

The following RocketLabWithWindowSeat component


( Shared/RocketLabWithWindowSeat.razor ) has been updated from the preceding example
to include a component parameter named WindowSeat to specify if the passenger
prefers a window seat on their flight:

Shared/RocketLabWithWindowSeat.razor :
razor

<h2>Rocket Lab®</h2>

<p>
User selected a window seat: @WindowSeat
</p>

<p>
Rocket Lab is a trademark of
<a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
[Parameter]
public bool WindowSeat { get; set; }
}

In the following example:

Only the RocketLabWithWindowSeat component's parameter for a window seat


( WindowSeat ) receives the value of the Window Seat checkbox.
The namespace of the app is BlazorSample . Change the namespace to match your
app's namespace.
The dynamically-rendered components are shared components in the app's
Shared folder:

Shown in this article section: RocketLabWithWindowSeat


( Shared/RocketLabWithWindowSeat.razor )
Components shown in the Example section earlier in this article:
SpaceX ( Shared/SpaceX.razor )

UnitedLaunchAlliance ( Shared/UnitedLaunchAlliance.razor )
VirginGalactic ( Shared/VirginGalactic.razor )

Pages/DynamicComponentExample2.razor :

razor

@page "/dynamiccomponent-example-2"

<h1><code>DynamicComponent</code> Component Example 2</h1>

<p>
<label>
<input type="checkbox" @bind="WindowSeat" />
Window Seat (Rocket Lab only)
</label>
</p>
<p>
<label>
Select your transport:
<select @onchange="OnDropdownChange">
<option value="">Select a value</option>
@foreach (var c in components)
{
<option value="@c.Key">@c.Value.Name</option>
}
</select>
</label>
</p>

@if (selectedType is not null)


{
<div class="border border-primary my-1 p-1">
<DynamicComponent Type="@selectedType"
Parameters="@components[selectedType.Name].Parameters" />
</div>
}

@code {
private Dictionary<string, ComponentMetadata> components =
new()
{
{
"RocketLabWithWindowSeat",
new ComponentMetadata
{
Name = "Rocket Lab with Window Seat",
Parameters = new() { { "WindowSeat", false } }
}
},
{
"VirginGalactic",
new ComponentMetadata { Name = "Virgin Galactic" }
},
{
"UnitedLaunchAlliance",
new ComponentMetadata { Name = "ULA" }
},
{
"SpaceX",
new ComponentMetadata { Name = "SpaceX" }
}
};
private Type? selectedType;
private bool windowSeat;

private bool WindowSeat


{
get { return windowSeat; }
set
{
windowSeat = value;
components[nameof(RocketLabWithWindowSeat)].Parameters["WindowSeat"] =
windowSeat;
}
}

private void OnDropdownChange(ChangeEventArgs e)


{
selectedType = e.Value?.ToString()?.Length > 0 ?
Type.GetType($"BlazorSample.Shared.{e.Value}") : null;
}
}

Event callbacks ( EventCallback )


Event callbacks (EventCallback) can be passed to a DynamicComponent in its parameter
dictionary.

ComponentMetadata.cs :

C#

public class ComponentMetadata


{
public string? Name { get; set; }
public Dictionary<string, object> Parameters { get; set; } =
new Dictionary<string, object>();
}

Implement an event callback parameter (EventCallback) within each dynamically-


rendered component.

Shared/RocketLab2.razor :

razor

<h2>Rocket Lab®</h2>

<p>
Rocket Lab is a registered trademark of
<a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

<button @onclick="OnClickCallback">
Trigger a Parent component method
</button>

@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}

Shared/SpaceX2.razor :

razor

<h2>SpaceX®</h2>

<p>
SpaceX is a registered trademark of
<a href="https://www.spacex.com/">Space Exploration Technologies Corp.
</a>
</p>

<button @onclick="OnClickCallback">
Trigger a Parent component method
</button>

@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}

Shared/UnitedLaunchAlliance2.razor :

razor

<h2>United Launch Alliance®</h2>

<p>
United Launch Alliance and ULA are registered trademarks of
<a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>

<button @onclick="OnClickCallback">
Trigger a Parent component method
</button>

@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}

Shared/VirginGalactic2.razor :

razor

<h2>Virgin Galactic®</h2>
<p>
Virgin Galactic is a registered trademark of
<a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>

<button @onclick="OnClickCallback">
Trigger a Parent component method
</button>

@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}

In the following parent component example, the ShowDTMessage method assigns a string
with the current time to message , and the value of message is rendered.

The parent component passes the callback method, ShowDTMessage in the parameter
dictionary:

The string key is the callback method's name, OnClickCallback .


The object value is created by EventCallbackFactory.Create for the parent callback
method, ShowDTMessage . Note that the this keyword isn't supported in C# fields, so
a C# property is used for the parameter dictionary.

For the following component, change the namespace name of BlazorSample in the
OnDropdownChange method to match your app's namespace.

Pages/DynamicComponentExample3.razor :

razor

@page "/dynamiccomponent-example-3"

<h1><code>DynamicComponent</code> Component Example 3</h1>

<p>
<label>
Select your transport:
<select @onchange="OnDropdownChange">
<option value="">Select a value</option>
<option value="@nameof(RocketLab2)">Rocket Lab</option>
<option value="@nameof(SpaceX2)">SpaceX</option>
<option value="@nameof(UnitedLaunchAlliance2)">ULA</option>
<option value="@nameof(VirginGalactic2)">Virgin
Galactic</option>
</select>
</label>
</p>
@if (selectedType is not null)
{
<div class="border border-primary my-1 p-1">
<DynamicComponent Type="@selectedType"
Parameters="@Components[selectedType.Name].Parameters" />
</div>
}

<p>
@message
</p>

@code {
private Type? selectedType;
private string? message;

private Dictionary<string, ComponentMetadata> Components


{
get
{
return new Dictionary<string, ComponentMetadata>()
{
{
"RocketLab2",
new ComponentMetadata
{
Name = "Rocket Lab",
Parameters =
new()
{
{
"OnClickCallback",

EventCallback.Factory.Create<MouseEventArgs>(
this, ShowDTMessage)
}
}
}
},
{
"VirginGalactic2",
new ComponentMetadata
{
Name = "Virgin Galactic",
Parameters =
new()
{
{
"OnClickCallback",

EventCallback.Factory.Create<MouseEventArgs>(
this, ShowDTMessage)
}
}
}
},
{
"UnitedLaunchAlliance2",
new ComponentMetadata
{
Name = "ULA",
Parameters =
new()
{
{
"OnClickCallback",

EventCallback.Factory.Create<MouseEventArgs>(
this, ShowDTMessage)
}
}
}
},
{
"SpaceX2",
new ComponentMetadata
{
Name = "SpaceX",
Parameters =
new()
{
{
"OnClickCallback",

EventCallback.Factory.Create<MouseEventArgs>(
this, ShowDTMessage)
}
}
}
}
};
}
}

private void OnDropdownChange(ChangeEventArgs e)


{
selectedType = e.Value?.ToString()?.Length > 0 ?
Type.GetType($"BlazorSample.Shared.{e.Value}") : null;
}

private void ShowDTMessage(MouseEventArgs e) =>


message = $"The current DT is: {DateTime.Now}.";
}

Avoid catch-all parameters


Avoid the use of catch-all parameters. If catch-all parameters are used, every explicit
parameter on DynamicComponent effectively is a reserved word that you can't pass to a
dynamic child. Any new parameters passed to DynamicComponent are a breaking
change, as they start shadowing child component parameters that happen to have the
same name. It's unlikely that the caller always knows a fixed set of parameter names to
pass to all possible dynamic children.

Trademarks
Rocket Lab is a registered trademark of Rocket Lab USA Inc. SpaceX is a registered
trademark of Space Exploration Technologies Corp. United Launch Alliance and ULA
are registered trademarks of United Launch Alliance, LLC . Virgin Galactic is a
registered trademark of Galactic Enterprises, LLC .

Additional resources
ASP.NET Core Blazor event handling
ASP.NET Core Blazor QuickGrid
component
Article • 03/30/2023

The QuickGrid component is an experimental Razor component for quickly and


efficiently displaying data in tabular form. QuickGrid provides a simple and convenient
data grid component for common grid rendering scenarios and serves as a reference
architecture and performance baseline for building data grid components. QuickGrid is
highly optimized and uses advanced techniques to achieve optimal rendering
performance.

To get started with QuickGrid :

Add package reference for Microsoft.AspNetCore.Components.QuickGrid . If using the


.NET CLI to add the package reference, include the --prerelease option when you
execute the dotnet add package command.

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 .

Add the following component to render a grid.

Pages/QuickGridExample.razor :

razor

@page "/quickgrid-example"
@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid Items="@people">
<PropertyColumn Property="@(p => p.PersonId)" Sortable="true" />
<PropertyColumn Property="@(p => p.Name)" Sortable="true" />
<PropertyColumn Property="@(p => p.PromotionDate)" Format="yyyy-MM-dd"
Sortable="true" />
</QuickGrid>

@code {
private record Person(int PersonId, string Name, DateOnly
PromotionDate);

private IQueryable<Person> people = new[]


{
new Person(10895, "Jean Martin", new DateOnly(1985, 3, 16)),
new Person(10944, "António Langa", new DateOnly(1991, 12, 1)),
new Person(11203, "Julie Smith", new DateOnly(1958, 10, 10)),
new Person(11205, "Nur Sari", new DateOnly(1922, 4, 27)),
new Person(11898, "Jose Hernandez", new DateOnly(2011, 5, 3)),
new Person(12130, "Kenji Sato", new DateOnly(2004, 1, 9)),
}.AsQueryable();
}

Access the component in a browser at the relative path /quickgrid-example .

There aren't current plans to extend QuickGrid with features that full-blown commercial
grids tend to offer, for example, hierarchical rows, drag-to-reorder columns, or Excel-like
range selections. If you require advanced features that you don't wish to develop on
your own, continue using third-party grids.

For various QuickGrid demonstrations, see the QuickGrid for Blazor app . The demo
site is built using Blazor WebAssembly and is hosted on GitHub Pages. The site loads
fast thanks to static prerendering using the community-maintained
BlazorWasmPrerendering.Build GitHub project .

2 Warning

The QuickGrid component is in preview for ASP.NET Core 7.x. You're welcome to
use it in production if it meets your needs, but it isn't officially supported until
ASP.NET Core 8.0 or later.
Prerender and integrate ASP.NET Core
Razor components
Article • 04/04/2023

This article explains Razor component integration scenarios for Blazor apps, including
prerendering of Razor components on the server.

) Important

Framework changes across ASP.NET Core releases led to different sets of


instructions in this article. Before using this article's guidance, confirm that the
document version selector on this page matches the version of ASP.NET Core that
you intend to use for your app.

Razor components can be integrated into Razor Pages and MVC apps. When the page
or view is rendered, components can be prerendered at the same time.

Prerendering can improve Search Engine Optimization (SEO) by rendering content for
the initial HTTP response that search engines can use to calculate page rank.

After configuring the project, use the guidance in the following sections depending on
the project's requirements:

For components that are directly routable from user requests. Follow this guidance
when visitors should be able to make an HTTP request in their browser for a
component with an @page directive.
Use routable components in a Razor Pages app
Use routable components in an MVC app
For components that aren't directly routable from user requests, see the Render
components from a page or view section. Follow this guidance when the app
embeds components into existing pages and views with the Component Tag
Helper.

Configuration
Use the following guidance to integrate Razor components into pages and views of an
existing Razor Pages or MVC app.

1. Add an imports file to the root folder of the project with the following content.
Change the {APP NAMESPACE} placeholder to the namespace of the project.
_Imports.razor :

razor

@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using {APP NAMESPACE}

2. In the project's layout file ( Pages/Shared/_Layout.cshtml in Razor Pages apps or


Views/Shared/_Layout.cshtml in MVC apps):

Add the following <base> tag and HeadOutlet component Tag Helper to the
<head> element:

CSHTML

<base href="~/" />


<component
type="typeof(Microsoft.AspNetCore.Components.Web.HeadOutlet)"
render-mode="ServerPrerendered" />

The href value (the app base path) in the preceding example assumes that
the app resides at the root URL path ( / ). If the app is a sub-application,
follow the guidance in the App base path section of the Host and deploy
ASP.NET Core Blazor article.

The HeadOutlet component is used to render head ( <head> ) content for page
titles (PageTitle component) and other head elements (HeadContent
component) set by Razor components. For more information, see Control
head content in ASP.NET Core Blazor apps.

Add a <script> tag for the blazor.server.js script immediately before the
Scripts render section ( @await RenderSectionAsync(...) ):

HTML

<script src="_framework/blazor.server.js"></script>
The framework adds the blazor.server.js script to the app. There's no need
to manually add a blazor.server.js script file to the app.

7 Note

Typically, the layout loads via a _ViewStart.cshtml file.

3. Register the Blazor Server services in Program.cs where services are registered:

C#

builder.Services.AddServerSideBlazor();

4. Add the Blazor Hub endpoint to the endpoints of Program.cs where routes are
mapped. Place the following line after the call to MapRazorPages (Razor Pages) or
MapControllerRoute (MVC):

C#

app.MapBlazorHub();

5. Integrate components into any page or view. For example, add a Counter
component to the project's Shared folder.

Pages/Shared/Counter.razor (Razor Pages) or Views/Shared/Counter.razor (MVC):

razor

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click


me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

Razor Pages:
In the project's Index page of a Razor Pages app, add the Counter component's
namespace and embed the component into the page. When the Index page loads,
the Counter component is prerendered in the page. In the following example,
replace the {APP NAMESPACE} placeholder with the project's namespace.

Pages/Index.cshtml :

CSHTML

@page
@using {APP NAMESPACE}.Pages.Shared
@model IndexModel
@{
ViewData["Title"] = "Home page";
}

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

MVC:

In the project's Index view of an MVC app, add the Counter component's
namespace and embed the component into the view. When the Index view loads,
the Counter component is prerendered in the page. In the following example,
replace the {APP NAMESPACE} placeholder with the project's namespace.

Views/Home/Index.cshtml :

CSHTML

@using {APP NAMESPACE}.Views.Shared


@{
ViewData["Title"] = "Home Page";
}

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

For more information, see the Render components from a page or view section.

Use routable components in a Razor Pages app


This section pertains to adding components that are directly routable from user requests.

To support routable Razor components in Razor Pages apps:

1. Follow the guidance in the Configuration section.


2. Add an App component to the project root with the following content.

App.razor :

razor

@using Microsoft.AspNetCore.Components.Routing

<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<p role="alert">Sorry, there's nothing at this address.</p>
</NotFound>
</Router>

3. Add a _Host page to the project with the following content. Replace the {APP
NAMESPACE} placeholder with the app's namespace.

Pages/_Host.cshtml :

CSHTML

@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

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

7 Note

The preceding example assumes that the HeadOutlet component and Blazor
script ( _framework/blazor.server.js ) are rendered by the app's layout. For
more information, see the Configuration section.

RenderMode configures whether the App component:

Is prerendered into the page.


Is rendered as static HTML on the page or if it includes the necessary
information to bootstrap a Blazor app from the user agent.

For more information on the Component Tag Helper, including passing parameters
and RenderMode configuration, see Component Tag Helper in ASP.NET Core.
4. In the Program.cs endpoints, add a low-priority route for the _Host page as the
last endpoint:

C#

app.MapFallbackToPage("/_Host");

5. Add routable components to the project. The following example is a


RoutableCounter component based on the Counter component in the Blazor
project templates.

Pages/RoutableCounter.razor :

razor

@page "/routable-counter"

<PageTitle>Routable Counter</PageTitle>

<h1>Routable Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click


me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

6. Run the project and navigate to the routable RoutableCounter component at


/routable-counter .

For more information on namespaces, see the Component namespaces section.

Use routable components in an MVC app


This section pertains to adding components that are directly routable from user requests.

To support routable Razor components in MVC apps:

1. Follow the guidance in the Configuration section.


2. Add an App component to the project root with the following content.

App.razor :

razor

@using Microsoft.AspNetCore.Components.Routing

<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<p role="alert">Sorry, there's nothing at this address.</p>
</NotFound>
</Router>

3. Add a _Host view to the project with the following content. Replace the {APP
NAMESPACE} placeholder with the app's namespace.

Views/Home/_Host.cshtml :

CSHTML

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

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

7 Note

The preceding example assumes that the HeadOutlet component and Blazor
script ( _framework/blazor.server.js ) are rendered by the app's layout. For
more information, see the Configuration section.

RenderMode configures whether the App component:

Is prerendered into the page.


Is rendered as static HTML on the page or if it includes the necessary
information to bootstrap a Blazor app from the user agent.

For more information on the Component Tag Helper, including passing parameters
and RenderMode configuration, see Component Tag Helper in ASP.NET Core.

4. Add an action to the Home controller.


Controllers/HomeController.cs :

C#

public IActionResult Blazor()


{
return View("_Host");
}

5. In the Program.cs endpoints, add a low-priority route for the controller action that
returns the _Host view:

C#

app.MapFallbackToController("Blazor", "Home");

6. Create a Pages folder in the MVC app and add routable components. The
following example is a RoutableCounter component based on the Counter
component in the Blazor project templates.

Pages/RoutableCounter.razor :

razor

@page "/routable-counter"

<PageTitle>Routable Counter</PageTitle>

<h1>Routable Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click


me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

7. Run the project and navigate to the routable RoutableCounter component at


/routable-counter .
For more information on namespaces, see the Component namespaces section.

Render components from a page or view


This section pertains to adding components to pages or views, where the components
aren't directly routable from user requests.

To render a component from a page or view, use the Component Tag Helper.

Render stateful interactive components


Stateful interactive components can be added to a Razor page or view.

When the page or view renders:

The component is prerendered with the page or view.


The initial component state used for prerendering is lost.
New component state is created when the SignalR connection is established.

The following Razor page renders a Counter component:

CSHTML

<h1>Razor Page</h1>

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


param-InitialValue="InitialValue" />

@functions {
[BindProperty(SupportsGet=true)]
public int InitialValue { get; set; }
}

For more information, see Component Tag Helper in ASP.NET Core.

Render noninteractive components


In the following Razor page, the Counter component is statically rendered with an initial
value that's specified using a form. Since the component is statically rendered, the
component isn't interactive:

CSHTML

<h1>Razor Page</h1>
<form>
<input type="number" asp-for="InitialValue" />
<button type="submit">Set initial value</button>
</form>

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


param-InitialValue="InitialValue" />

@functions {
[BindProperty(SupportsGet=true)]
public int InitialValue { get; set; }
}

For more information, see Component Tag Helper in ASP.NET Core.

Component namespaces
When using a custom folder to hold the project's Razor components, add the
namespace representing the folder to either the page/view or to the
_ViewImports.cshtml file. In the following example:

Components are stored in the Components folder of the project.


The {APP NAMESPACE} placeholder is the project's namespace. Components
represents the name of the folder.

CSHTML

@using {APP NAMESPACE}.Components

The _ViewImports.cshtml file is located in the Pages folder of a Razor Pages app or the
Views folder of an MVC app.

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

Persist prerendered state


Without persisting prerendered state, state used during prerendering is lost and must
be recreated when the app is fully loaded. If any state is setup asynchronously, the UI
may flicker as the prerendered UI is replaced with temporary placeholders and then fully
rendered again.

To solve these problems, Blazor supports persisting state in a prerendered page using
the Persist Component State Tag Helper. Add the Tag Helper's tag, <persist-component-
state /> , inside the closing </body> tag.
Pages/_Host.cshtml :

CSHTML

<body>
...

<persist-component-state />
</body>

Decide what state to persist using the PersistentComponentState service.


PersistentComponentState.RegisterOnPersisting registers a callback to persist the
component state before the app is paused. The state is retrieved when the application
resumes.

The following example is an updated version of the FetchData component in a hosted


Blazor WebAssembly app based on the Blazor project template. The
WeatherForecastPreserveState component persists weather forecast state during

prerendering and then retrieves the state to initialize the component. The Persist
Component State Tag Helper persists the component state after all component
invocations.

Pages/WeatherForecastPreserveState.razor :

razor

@page "/weather-forecast-preserve-state"
@implements IDisposable
@using BlazorSample.Shared
@inject IWeatherForecastService WeatherForecastService
@inject PersistentComponentState ApplicationState

<PageTitle>Weather Forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)


{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}

@code {
private WeatherForecast[] forecasts = Array.Empty<WeatherForecast>();
private PersistingComponentStateSubscription persistingSubscription;

protected override async Task OnInitializedAsync()


{
persistingSubscription =
ApplicationState.RegisterOnPersisting(PersistForecasts);

if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>(
"fetchdata", out var restored))
{
forecasts =
await
WeatherForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now))
;
}
else
{
forecasts = restored!;
}
}

private Task PersistForecasts()


{
ApplicationState.PersistAsJson("fetchdata", forecasts);

return Task.CompletedTask;
}

void IDisposable.Dispose()
{
persistingSubscription.Dispose();
}
}
By initializing components with the same state used during prerendering, any expensive
initialization steps are only executed once. The rendered UI also matches the
prerendered UI, so no flicker occurs in the browser.

Prerendered state size and SignalR message


size limit
A large prerendered state size may exceed the SignalR circuit message size limit, which
results in the following:

The SignalR circuit fails to initialize with an error on the client: Circuit host not
initialized.
The reconnection dialog on the client appears when the circuit fails. Recovery isn't
possible.

To resolve the problem, use either of the following approaches:

Reduce the amount of data that you are putting into the prerendered state.
Increase the SignalR message size limit. WARNING: Increasing the limit may
increase the risk of Denial of service (DoS) attacks.

Additional Blazor Server resources


State management: Handle prerendering
Razor component lifecycle subjects that pertain to prerendering
Component initialization (OnInitialized{Async})
After component render (OnAfterRender{Async})
Stateful reconnection after prerendering
Prerendering with JavaScript interop
Authentication and authorization: General aspects
Handle Errors: Prerendering
Host and deploy: Blazor Server
Threat mitigation: Cross-site scripting (XSS)
Consume ASP.NET Core Razor
components from a Razor class library
(RCL)
Article • 06/01/2023

Components can be shared in a Razor class library (RCL) across projects. Include
components and static assets in an app from:

Another project in the solution.


A referenced .NET library.
A NuGet package.

Just as components are regular .NET types, components provided by an RCL are normal
.NET assemblies.

Create an RCL
Visual Studio

1. Create a new project.


2. In the Create a new project dialog, select Razor Class Library from the list of
ASP.NET Core project templates. Select Next.
3. In the Configure your new project dialog, provide a project name in the
Project name field or accept the default project name. Examples in this topic
use the project name ComponentLibrary . Select Create.
4. In the Create a new Razor class library dialog, select Create.
5. Add the RCL to a solution:
a. Open the solution.
b. Right-click the solution in Solution Explorer. Select Add > Existing Project.
c. Navigate to the RCL's project file.
d. Select the RCL's project file ( .csproj ).
6. Add a reference to the RCL from the app:
a. Right-click the app project. Select Add > Project Reference.
b. Select the RCL project. Select OK.

If the Support pages and views checkbox is selected to support pages and views
when generating the RCL from the template:
Add an _Imports.razor file to root of the generated RCL project with the
following contents to enable Razor component authoring:

razor

@using Microsoft.AspNetCore.Components.Web

Add the following SupportedPlatform item to the project file ( .csproj ):

XML

<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>

For more information on the SupportedPlatform item, see the Browser


compatibility analyzer for Blazor WebAssembly section.

Consume a Razor component from an RCL


To consume components from an RCL in another project, use either of the following
approaches:

Use the full component type name, which includes the RCL's namespace.
Individual components can be added by name without the RCL's namespace if
Razor's @using directive declares the RCL's namespace. Use the following
approaches:
Add the @using directive to individual components.
include the @using directive in the top-level _Imports.razor file to make the
library's components available to an entire project. Add the directive to an
_Imports.razor file at any level to apply the namespace to a single component
or set of components within a folder. When an _Imports.razor file is used,
individual components don't require an @using directive for the RCL's
namespace.

In the following examples, ComponentLibrary is an RCL containing the Component1


component. The Component1 component is an example component automatically added
to an RCL created from the RCL project template that isn't created to support pages and
views.
7 Note

If the RCL is created to support pages and views, manually add the Component1
component and its static assets to the RCL if you plan to follow the examples in this
article. The component and static assets are shown in this section.

Component1.razor in the ComponentLibrary RCL:

razor

<div class="my-component">
This component is defined in the <strong>ComponentLibrary</strong>
package.
</div>

In the app that consumes the RCL, reference the Component1 component using its
namespace, as the following example shows.

Pages/ConsumeComponent1.razor :

razor

@page "/consume-component-1"

<h1>Consume component (full namespace example)</h1>

<ComponentLibrary.Component1 />

Alternatively, add a @using directive and use the component without its namespace.
The following @using directive can also appear in any _Imports.razor file in or above
the current folder.

Pages/ConsumeComponent2.razor :

razor

@page "/consume-component-2"
@using ComponentLibrary

<h1>Consume component (<code>@@using</code> example)</h1>

<Component1 />
For library components that use CSS isolation, the component styles are automatically
made available to the consuming app. There's no need to manually link or import the
library's individual component stylesheets or its bundled CSS file in the app that
consumes the library. The app uses CSS imports to reference the RCL's bundled styles.
The bundled styles aren't published as a static web asset of the app that consumes the
library. For a class library named ClassLib and a Blazor app with a
BlazorSample.styles.css stylesheet, the RCL's stylesheet is imported at the top of the
app's stylesheet automatically at build time:

css

@import '_content/ClassLib/ClassLib.bundle.scp.css';

For the preceding examples, Component1 's stylesheet ( Component1.razor.css ) is bundled


automatically.

Component1.razor.css in the ComponentLibrary RCL:

css

.my-component {
border: 2px dashed red;
padding: 1em;
margin: 1em 0;
background-image: url('background.png');
}

The background image is also included from the RCL project template and resides in the
wwwroot folder of the RCL.

wwwroot/background.png in the ComponentLibrary RCL:

To provide additional library component styles from stylesheets in the library's wwwroot
folder, add stylesheet <link> tags to the RCL's consumer, as the next example
demonstrates.

) Important

Generally, library components use CSS isolation to bundle and provide component
styles. Component styles that rely upon CSS isolation are automatically made
available to the app that uses the RCL. There's no need to manually link or import
the library's individual component stylesheets or its bundled CSS file in the app that
consumes the library. The following example is for providing global stylesheets
outside of CSS isolation, which usually isn't a requirement for typical apps that
consume RCLs.

The following background image is used in the next example. If you implement the
example shown in this section, right-click the image to save it locally.

wwwroot/extra-background.png in the ComponentLibrary RCL:

Add a new stylesheet to the RCL with an extra-style class.

wwwroot/additionalStyles.css in the ComponentLibrary RCL:

css

.extra-style {
border: 2px dashed blue;
padding: 1em;
margin: 1em 0;
background-image: url('extra-background.png');
}

Add a component to the RCL that uses the extra-style class.

ExtraStyles.razor in the ComponentLibrary RCL:

razor

<div class="extra-style">
<p>
This component is defined in the <strong>ComponentLibrary</strong>
package.
</p>
</div>

Add a page to the app that uses the ExtraStyles component from the RCL.

Pages/ConsumeComponent3.razor :

razor
@page "/consume-component-3"
@using ComponentLibrary

<h1>Consume component (<code>additionalStyles.css</code> example)</h1>

<ExtraStyles />

Link to the library's stylesheet in the app's <head> markup (location of <head> content).

HTML

<link href="_content/ComponentLibrary/additionalStyles.css" rel="stylesheet"


/>

Create an RCL with static assets in the wwwroot


folder
An RCL's static assets are available to any app that consumes the library.

Place static assets in the wwwroot folder of the RCL and reference the static assets with
the following path in the app: _content/{PACKAGE ID}/{PATH AND FILE NAME} . The
{PACKAGE ID} placeholder is the library's package ID. The package ID defaults to the
project's assembly name if <PackageId> isn't specified in the project file. The {PATH AND
FILE NAME} placeholder is path and file name under wwwroot . This path format is also
used in the app for static assets supplied by NuGet packages added to the RCL.

The following example demonstrates the use of RCL static assets with an RCL named
ComponentLibrary and a Blazor app that consumes the RCL. The app has a project
reference for the ComponentLibrary RCL.

The following Jeep® image is used in this section's example. If you implement the
example shown in this section, right-click the image to save it locally.

wwwroot/jeep-yj.png in the ComponentLibrary RCL:


Add the following JeepYJ component to the RCL.

JeepYJ.razor in the ComponentLibrary RCL:

razor

<h3>ComponentLibrary.JeepYJ</h3>

<p>
<img alt="Jeep YJ&reg;" src="_content/ComponentLibrary/jeep-yj.png" />
</p>

Add the following Jeep component to the app that consumes the ComponentLibrary
RCL. The Jeep component uses:

The Jeep YJ® image from the ComponentLibrary RCL's wwwroot folder.
The JeepYJ component from the RCL.

Pages/Jeep.razor :

razor

@page "/jeep"
@using ComponentLibrary

<div style="float:left;margin-right:10px">
<h3>Direct use</h3>

<p>
<img alt="Jeep YJ&reg;" src="_content/ComponentLibrary/jeep-yj.png"
/>
</p>
</div>

<JeepYJ />

<p>
<em>Jeep</em> and <em>Jeep YJ</em> are registered trademarks of
<a href="https://www.stellantis.com">FCA US LLC (Stellantis NV)</a>.
</p>

Rendered Jeep component:

For more information, see Reusable Razor UI in class libraries with ASP.NET Core.

Create an RCL with JavaScript files collocated


with components
Collocation of JavaScript (JS) files for pages, views, and Razor components is a
convenient way to organize scripts in an app.

Collocate JS files using the following filename extension conventions:

Pages of Razor Pages apps and views of MVC apps: .cshtml.js . Examples:
Pages/Index.cshtml.js for the Index page of a Razor Pages app at

Pages/Index.cshtml .
Views/Home/Index.cshtml.js for the Index view of an MVC app at

Views/Home/Index.cshtml .
Razor components of Blazor apps: .razor.js . Example: Pages/Index.razor.js for
the Index component at Pages/Index.razor .

Collocated JS files are publicly addressable using the path to the file in the project:

Pages, views, and components from a collocated scripts file in the app:

{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js


The {PATH} placeholder is the path to the page, view, or component.
The {PAGE, VIEW, OR COMPONENT} placeholder is the page, view, or component.
The {EXTENSION} placeholder matches the extension of the page, view, or
component, either razor or cshtml .

Razor Pages example:

A JS file for the Index page is placed in the Pages folder ( Pages/Index.cshtml.js )
next to the Index page ( Pages/Index.cshtml ). In the Index page, the script is
referenced at the path in the Pages folder:

razor

@section Scripts {
<script src="~/Pages/Index.cshtml.js"></script>
}

When the app is published, the framework automatically moves the script to the
web root. In the preceding example, the script is moved to bin\Release\{TARGET
FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.cshtml.js , where the {TARGET
FRAMEWORK MONIKER} placeholder is the Target Framework Moniker (TFM). No

change is required to the script's relative URL in the Index page.

Blazor example:

A JS file for the Index component is placed in the Pages folder


( Pages/Index.razor.js ) next to the Index component ( Pages/Index.razor ). In the
Index component, the script is referenced at the path in the Pages folder.

Pages/Index.razor.js :

JavaScript

export function showPrompt(message) {


return prompt(message, 'Type anything here');
}

In the OnAfterRenderAsync method of the Index component ( Pages/Index.razor ):

razor

module = await JS.InvokeAsync<IJSObjectReference>(


"import", "./Pages/Index.razor.js");
When the app is published, the framework automatically moves the script to the
web root. In the preceding example, the script is moved to bin\Release\{TARGET
FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.razor.js , where the {TARGET

FRAMEWORK MONIKER} placeholder is the Target Framework Moniker (TFM). No


change is required to the script's relative URL in the Index component.

For scripts provided by a Razor class library (RCL):

_content/{PACKAGE ID}/{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js


The {PACKAGE ID} placeholder is the RCL's package identifier (or library name
for a class library referenced by the app).
The {PATH} placeholder is the path to the page, view, or component. If a Razor
component is located at the root of the RCL, the path segment isn't included.
The {PAGE, VIEW, OR COMPONENT} placeholder is the page, view, or component.
The {EXTENSION} placeholder matches the extension of page, view, or
component, either razor or cshtml .

In the following Blazor app example:


The RCL's package identifier is AppJS .
A module's scripts are loaded for the Index component ( Index.razor ).
The Index component is in the Pages folder of the RCL.

C#

var module = await JS.InvokeAsync<IJSObjectReference>("import",


"./_content/AppJS/Pages/Index.razor.js");

Supply components and static assets to


multiple hosted Blazor apps
For more information, see Host and deploy ASP.NET Core Blazor WebAssembly.

Browser compatibility analyzer for Blazor


WebAssembly
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 Blazor WebAssembly apps, this means
checking that APIs are supported in browsers. Annotating .NET framework APIs for the
compatibility analyzer is an on-going process, so not all .NET framework API is currently
annotated.

Blazor WebAssembly and RCL projects automatically enable browser compatibility


checks by adding browser as a supported platform with the SupportedPlatform MSBuild
item. Library developers can manually add the SupportedPlatform item to a library's
project file to enable the feature:

XML

<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>

When authoring a library, indicate that a particular API isn't supported in browsers by
specifying browser to UnsupportedOSPlatformAttribute:

C#

using System.Runtime.Versioning;

...

[UnsupportedOSPlatform("browser")]
private static string GetLoggingDirectory()
{
...
}

For more information, see Annotating APIs as unsupported on specific platforms


(dotnet/designs GitHub repository .

JavaScript isolation in JavaScript modules


Blazor enables JavaScript isolation in standard JavaScript modules . JavaScript isolation
provides the following benefits:

Imported JavaScript no longer pollutes the global namespace.


Consumers of the library and components aren't required to manually import the
related JavaScript.
For more information, see Call JavaScript functions from .NET methods in ASP.NET Core
Blazor.

Avoid trimming JavaScript-invokable .NET


methods
Runtime relinking trims class instance JavaScript-invokable .NET methods unless they're
explicitly preserved. For more information, see Call .NET methods from JavaScript
functions in ASP.NET Core Blazor.

Build, pack, and ship to NuGet


Because Razor class libraries that contain Razor components are standard .NET libraries,
packing and shipping them to NuGet is no different from packing and shipping any
library to NuGet. Packing is performed using the dotnet pack command in a command
shell:

.NET CLI

dotnet pack

Upload the package to NuGet using the dotnet nuget push command in a command
shell.

Trademarks
Jeep and Jeep YJ are registered trademarks of FCA US LLC (Stellantis NV) .

Additional resources
Reusable Razor UI in class libraries with ASP.NET Core
Add an XML Intermediate Language (IL) Trimmer configuration file to a library
CSS isolation support with Razor class libraries
Use Razor components in JavaScript
apps and SPA frameworks
Article • 03/08/2023

This article covers how to render Razor components from JavaScript, use Blazor custom
elements, and generate Angular and React components.

Render Razor components from JavaScript


Razor components can be dynamically-rendered from JavaScript (JS) for existing JS
apps.

The example in this section renders the following Razor component into a page via JS.

Shared/Quote.razor :

razor

<div class="m-5 p-5">


<h2>Quote</h2>
<p>@Text</p>
</div>

@code {
[Parameter]
public string? Text { get; set; }
}

In Program.cs , add the namespace for the location of the component. The following
example assumes that the Quote component is in the app's Shared folder, and the app's
namespace is BlazorSample :

C#

using BlazorSample.Shared;

Call RegisterForJavaScript on the app's root component collection to register the a


Razor component as a root component for JS rendering.

RegisterForJavaScript includes an overload that accepts the name of a JS function that


executes initialization logic ( javaScriptInitializer ). The JS function is called once per
component registration immediately after the Blazor app starts and before any
components are rendered. This function can be used for integration with JS
technologies, such as HTML custom elements or a JS-based SPA framework.

One or more initializer functions can be created and called by different component
registrations. The typical use case is to reuse the same initializer function for multiple
components, which is expected if the initializer function is configuring integration with
custom elements or another JS-based SPA framework.

) Important

Don't confuse the javaScriptInitializer parameter of RegisterForJavaScript with


JavaScript initializers. The name of the parameter and the JS initializers feature is
coincidental.

The following example demonstrates the dynamic registration of the preceding Quote
component with " quote " as the identifier.

In a Blazor Server app, modify the call to AddServerSideBlazor in Program.cs :

C#

builder.Services.AddServerSideBlazor(options =>
{
options.RootComponents.RegisterForJavaScript<Quote>(identifier:
"quote",
javaScriptInitializer: "initializeComponent");
});

In a Blazor WebAssembly app, call RegisterForJavaScript on RootComponents in


Program.cs :

C#

builder.RootComponents.RegisterForJavaScript<Quote>(identifier:
"quote",
javaScriptInitializer: "initializeComponent");

Attach the initializer function with name and parameters function parameters to the
window object. For demonstration purposes, the following initializeComponent function

logs the name and parameters of the registered component.

wwwroot/js/jsComponentInitializers.js :

JavaScript
window.initializeComponent = (name, parameters) => {
console.log({ name: name, parameters: parameters });
}

Render the component from JS into a container element using the registered identifier,
passing component parameters as needed.

In the following example:

The Quote component ( quote identifier) is rendered into the quoteContainer


element when the showQuote function is called.
A quote string is passed to the component's Text parameter.

wwwroot/js/scripts.js :

JavaScript

async function showQuote() {


let targetElement = document.getElementById('quoteContainer');
await Blazor.rootComponents.add(targetElement, 'quote',
{
text: "Crow: I have my doubts that this movie is actually 'starring' " +
"anybody. More like, 'camera is generally pointed at.'"
});
}

Load Blazor ( blazor.server.js or blazor.webassembly.js ) with the preceding scripts


into the JS app:

HTML

<script src="_framework/blazor.{server|webassembly}.js"></script>
<script src="js/jsComponentInitializers.js"></script>
<script src="js/scripts.js"></script>

In HTML, place the target container element ( quoteContainer ). For the demonstration in
this section, a button triggers rendering the Quote component by calling the showQuote
JS function:

HTML

<button onclick="showQuote()">Show Quote</button>

<div id="quoteContainer"></div>
On initialization before any components are rendered, the browser's developer tools
console logs the Quote component's identifier ( name ) and parameters ( parameters )
when initializeComponent is called:

Console

Object { name: "quote", parameters: (1) […] }


name: "quote"
parameters: Array [ {…} ]
0: Object { name: "Text", type: "string" }
length: 1

When the Show Quote button is selected, the Quote component is rendered with the
quote stored in Text displayed:

Quote ©1988-1999 Satellite of Love LLC: Mystery Science Theater 3000 (Trace Beaulieu
(Crow) )

7 Note

rootComponents.add returns an instance of the component. Call dispose on the

instance to release it:

JavaScript

const rootComponent = await window.Blazor.rootComponents.add(...);

...

rootComponent.dispose();

The preceding example dynamically renders the root component when the showQuote()
JS function is called. To render a root component into a container element when Blazor
starts, use a JavaScript initializer to render the component, as the following example
demonstrates.

The following example builds on the preceding example, using the Quote component,
the root component registration in Program.cs , and the initialization of
jsComponentInitializers.js . The showQuote() function (and the script.js file) aren't

used.
In HTML, place the target container element, quoteContainer2 for this example:

HTML

<div id="quoteContainer2"></div>

Using a JavaScript initializer, add the root component to the target container element.

wwwroot/{PACKAGE ID/ASSEMBLY NAME}.lib.module.js :

JavaScript

export function afterStarted(blazor) {


let targetElement = document.getElementById('quoteContainer2');
blazor.rootComponents.add(targetElement, 'quote',
{
text: "Crow: I have my doubts that this movie is actually 'starring' "
+
"anybody. More like, 'camera is generally pointed at.'"
});
}

7 Note

For the call to rootComponents.add , use the blazor parameter (lowercase b )


provided by afterStarted . Although the registration is valid when using the Blazor
object (uppercase B ), the preferred approach is to use the parameter.

For an advanced example with additional features, see the example in the BasicTestApp
of the ASP.NET Core reference source ( dotnet/aspnetcore GitHub repository):

JavaScriptRootComponents.razor
wwwroot/js/jsRootComponentInitializers.js
wwwroot/index.html

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .
Blazor custom elements
Use Blazor custom elements to dynamically render Razor components from other SPA
frameworks, such as Angular or React.

Blazor custom elements:

Use standard HTML interfaces to implement custom HTML elements.


Eliminate the need to manually manage the state and lifecycle of root Razor
components using JavaScript APIs.
Are useful for gradually introducing Razor components into existing projects
written in other SPA frameworks.

Custom elements don't support child content or templated components.

Element name
Per the HTML specification , custom element tag names must adopt kebab case:

❌ mycounter
❌ MY-COUNTER
❌ MyCounter
✔️ my-counter
✔️ my-cool-counter

Package
Add a package reference for Microsoft.AspNetCore.Components.CustomElements to
the app's project file.

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 .

Blazor Server registration


To register a root component as a custom element in a Blazor Server app, modify the
call to AddServerSideBlazor in Program.cs . The following example registers the Counter
component with the custom HTML element my-counter :
C#

builder.Services.AddServerSideBlazor(options =>
{
options.RootComponents.RegisterCustomElement<Counter>("my-counter");
});

7 Note

The preceding code example requires a namespace for the app's components (for
example, using BlazorSample.Pages; ) in the Program.cs file.

Blazor WebAssembly registration


To register a root component as a custom element in a Blazor WebAssembly app, call
RegisterCustomElement on RootComponents in Program.cs . The following example

registers the Counter component with the custom HTML element my-counter :

C#

builder.RootComponents.RegisterCustomElement<Counter>("my-counter");

7 Note

The preceding code example requires a namespace for the app's components (for
example, using BlazorSample.Pages; ) in the Program.cs file.

Use the registered custom element


Use the custom element with any web framework. For example, the preceding my-
counter custom HTML element that renders the app's Counter component is used in a
React app with the following markup:

HTML

<my-counter></my-counter>

For a complete example of how to create custom elements with Blazor, see the
CustomElementsComponent component in the reference source.
7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Pass parameters
Pass parameters to your Blazor component either as HTML attributes or as JavaScript
properties on the DOM element.

The following Counter component uses an IncrementAmount parameter to set the


increment amount of the Click me button.

Pages/Counter.razor :

razor

@page "/counter"

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

[Parameter]
public int IncrementAmount { get; set; } = 1;

private void IncrementCount()


{
currentCount += IncrementAmount;
}
}

Render the Counter component with the custom element and pass a value to the
IncrementAmount parameter as an HTML attribute. The attribute name adopts kebab-
case syntax ( increment-amount , not IncrementAmount ):

HTML
<my-counter increment-amount="10"></my-counter>

Alternatively, you can set the parameter's value as a JavaScript property on the element
object. The property name adopts camel case syntax ( incrementAmount , not
IncrementAmount ):

JavaScript

const elem = document.querySelector("my-counter");


elem.incrementAmount = 10;

You can update parameter values at any time using either attribute or property syntax.

Supported parameter types:

Using JavaScript property syntax, you can pass objects of any JSON-serializable
type.
Using HTML attributes, you are limited to passing objects of string, boolean, or
numerical types.

Generate Angular and React components


Generate framework-specific JavaScript (JS) components from Razor components for
web frameworks, such as Angular or React. This capability isn't included with .NET, but is
enabled by the support for rendering Razor components from JS. The JS component
generation sample on GitHub demonstrates how to generate Angular and React
components from Razor components. See the GitHub sample app's README.md file for
additional information.

2 Warning

The Angular and React component features are currently experimental,


unsupported, and subject to change or be removed at any time. We welcome
your feedback on how well this particular approach meets your requirements.
ASP.NET Core built-in Razor
components
Article • 04/27/2023

The following built-in Razor components are provided by the Blazor framework:

App
Authentication
AuthorizeView
CascadingValue
DynamicComponent
ErrorBoundary
FocusOnNavigate
HeadContent
HeadOutlet
InputCheckbox
InputDate
InputFile
InputNumber
InputRadio
InputRadioGroup
InputSelect
InputText
InputTextArea
LayoutView
MainLayout
NavLink
NavMenu
PageTitle
QuickGrid
Router
RouteView
Virtualize
ASP.NET Core Blazor globalization and
localization
Article • 06/07/2023

This article explains how to render globalized and localized content to users in different
cultures and languages in a Blazor Server app. See the Blazor WebAssembly version of
this article for guidance on standalone and hosted Blazor WebAssembly apps.

For globalization, Blazor provides number and date formatting. For localization, Blazor
renders content using the .NET Resources system.

A limited set of ASP.NET Core's localization features are supported:

✔️IStringLocalizer and IStringLocalizer<T> are supported in Blazor apps.

❌ IHtmlLocalizer, IViewLocalizer, and Data Annotations localization are ASP.NET Core


MVC features and not supported in Blazor apps.

This article describes how to use Blazor's globalization and localization features based
on:

The Accept-Language header , which is set by the browser based on a user's


language preferences in browser settings.
A culture set by the app not based on the value of the Accept-Language header .
The setting can be static for all users or dynamic based on app logic. When the
setting is based on the user's preference, the setting is usually saved for reload on
future visits.

For additional general information, see the following resources:

Globalization and localization in ASP.NET Core


.NET Fundamentals: Globalization
.NET Fundamentals: Localization

Often, the terms language and culture are used interchangeably when dealing with
globalization and localization concepts.

In this article, language refers to selections made by a user in their browser's settings.
The user's language selections are submitted in browser requests in the Accept-
Language header . Browser settings usually use the word "language" in the UI.

Culture pertains to members of .NET and Blazor API. For example, a user's request can
include the Accept-Language header specifying a language from the user's
perspective, but the app ultimately sets the CurrentCulture ("culture") property from the
language that the user requested. API usually uses the word "culture" in its member
names.

7 Note

The code examples in this article adopt nullable reference types (NRTs) and .NET
compiler null-state static analysis, which are supported in ASP.NET Core 6.0 or
later. When targeting ASP.NET Core 5.0 or earlier, remove the null type designation
( ? ) from the article's examples.

Globalization
The @bind attribute directive applies formats and parses values for display based on the
user's first preferred language that the app supports. @bind supports the
@bind:culture parameter to provide a System.Globalization.CultureInfo for parsing and
formatting a value.

The current culture can be accessed from the


System.Globalization.CultureInfo.CurrentCulture property.

CultureInfo.InvariantCulture is used for the following field types ( <input type="{TYPE}"


/> , where the {TYPE} placeholder is the type):

date

number

The preceding field types:

Are displayed using their appropriate browser-based formatting rules.


Can't contain free-form text.
Provide user interaction characteristics based on the browser's implementation.

When using the date and number field types, specifying a culture with @bind:culture
isn't recommended because Blazor provides built-in support to render values in the
current culture.

The following field types have specific formatting requirements and aren't currently
supported by Blazor because they aren't supported by all of the major browsers:

datetime-local
month
week

For current browser support of the preceding types, see Can I use .

Invariant globalization
If the app doesn't require localization, configure the app to support the invariant culture,
which is generally based on United States English ( en-US ). Set the
InvariantGlobalization property to true in the app's project file ( .csproj ):

XML

<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

Alternatively, configure invariant globalization with the following approaches:

In runtimeconfig.json :

JSON

{
"runtimeOptions": {
"configProperties": {
"System.Globalization.Invariant": true
}
}
}

With an environment variable:


Key: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
Value: true or 1

For more information, see Runtime configuration options for globalization (.NET
documentation).

Demonstration component
The following CultureExample1 component can be used to demonstrate Blazor
globalization and localization concepts covered by this article.

Pages/CultureExample1.razor :
razor

@page "/culture-example-1"
@using System.Globalization

<h1>Culture Example 1</h1>

<p>
<b>CurrentCulture</b>: @CultureInfo.CurrentCulture
</p>

<h2>Rendered values</h2>

<ul>
<li><b>Date</b>: @dt</li>
<li><b>Number</b>: @number.ToString("N2")</li>
</ul>

<h2><code>&lt;input&gt;</code> elements that don't set a <code>type</code>


</h2>

<p>
The following <code>&lt;input&gt;</code> elements use
<code>CultureInfo.CurrentCulture</code>.
</p>

<ul>
<li><label><b>Date:</b> <input @bind="dt" /></label></li>
<li><label><b>Number:</b> <input @bind="number" /></label></li>
</ul>

<h2><code>&lt;input&gt;</code> elements that set a <code>type</code></h2>

<p>
The following <code>&lt;input&gt;</code> elements use
<code>CultureInfo.InvariantCulture</code>.
</p>

<ul>
<li><label><b>Date:</b> <input type="date" @bind="dt" /></label></li>
<li><label><b>Number:</b> <input type="number" @bind="number" /></label>
</li>
</ul>

@code {
private DateTime dt = DateTime.Now;
private double number = 1999.69;
}

The number string format ( N2 ) in the preceding example ( .ToString("N2") ) is a


standard .NET numeric format specifier. The N2 format is supported for all numeric
types, includes a group separator, and renders up to two decimal places.
Optionally, add a menu item to the navigation in Shared/NavMenu.razor for the
CultureExample1 component.

Dynamically set the culture from the Accept-


Language header
The Accept-Language header is set by the browser and controlled by the user's
language preferences in browser settings. In browser settings, a user sets one or more
preferred languages in order of preference. The order of preference is used by the
browser to set quality values ( q , 0-1) for each language in the header. The following
example specifies United States English, English, and Chilean Spanish with a preference
for United States English or English:

Accept-Language: en-US,en;q=0.9,es-CL;q=0.8

The app's culture is set by matching the first requested language that matches a
supported culture of the app.

Blazor Server apps are localized using Localization Middleware. Add localization services
to the app with AddLocalization.

In Program.cs :

C#

builder.Services.AddLocalization();

Specify the app's supported cultures in Program.cs immediately after Routing


Middleware is added to the processing pipeline. The following example configures
supported cultures for United States English and Chilean Spanish:

C#

app.UseRequestLocalization(new RequestLocalizationOptions()
.AddSupportedCultures(new[] { "en-US", "es-CL" })
.AddSupportedUICultures(new[] { "en-US", "es-CL" }));

For information on ordering the Localization Middleware in the middleware pipeline of


Program.cs , see ASP.NET Core Middleware.

Use the CultureExample1 component shown in the Demonstration component section


to study how globalization works. Issue a request with United States English ( en-US ).
Switch to Chilean Spanish ( es-CL ) in the browser's language settings. Request the
webpage again.

7 Note

Some browsers force you to use the default language setting for both requests and
the browser's own UI settings. This can make changing the language back to one
that you understand difficult because all of the setting UI screens might end up in a
language that you can't read. A browser such as Opera is a good choice for
testing because it permits you to set a default language for webpage requests but
leave the browser's settings UI in your language.

When the culture is United States English ( en-US ), the rendered component uses
month/day date formatting ( 6/7 ), 12-hour time ( AM / PM ), and comma separators in
numbers with a dot for the decimal value ( 1,999.69 ):

Date: 6/7/2021 6:45:22 AM


Number: 1,999.69

When the culture is Chilean Spanish ( es-CL ), the rendered component uses day/month
date formatting ( 7/6 ), 24-hour time, and period separators in numbers with a comma
for the decimal value ( 1.999,69 ):

Date: 7/6/2021 6:49:38


Number: 1.999,69

Statically set the culture


Blazor Server apps are localized using Localization Middleware. Add localization services
to the app with AddLocalization.

In Program.cs :

C#

builder.Services.AddLocalization();

Specify the static culture in Program.cs immediately after Routing Middleware is added
to the processing pipeline. The following example configures United States English:

C#
app.UseRequestLocalization("en-US");

The culture value for UseRequestLocalization must conform to the BCP-47 language tag
format .

For information on ordering the Localization Middleware in the middleware pipeline of


Program.cs , see ASP.NET Core Middleware.

Use the CultureExample1 component shown in the Demonstration component section


to study how globalization works. Issue a request with United States English ( en-US ).
Switch to Chilean Spanish ( es-CL ) in the browser's language settings. Request the
webpage again. When the requested language is Chilean Spanish, the app's culture
remains United States English ( en-US ).

Dynamically set the culture by user preference


Examples of locations where an app might store a user's preference include in browser
local storage (common in Blazor WebAssembly apps), in a localization cookie or
database (common in Blazor Server apps), or in an external service attached to an
external database and accessed by a web API. The following example demonstrates how
to use a localization cookie.

Add the Microsoft.Extensions.Localization package to the app.

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 .

Blazor Server apps are localized using Localization Middleware. Add localization services
to the app with AddLocalization.

In Program.cs :

C#

builder.Services.AddLocalization();

Set the app's default and supported cultures with RequestLocalizationOptions.


In Program.cs immediately after Routing Middleware is added to the processing
pipeline:

C#

var supportedCultures = new[] { "en-US", "es-CL" };


var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);

For information on ordering the Localization Middleware in the middleware pipeline of


Program.cs , see ASP.NET Core Middleware.

The following example shows how to set the current culture in a cookie that can be read
by the Localization Middleware.

Modifications to the Pages/_Host.cshtml file require the following namespaces:

System.Globalization
Microsoft.AspNetCore.Localization

Pages/_Host.cshtml :

diff

+ @using System.Globalization
+ @using Microsoft.AspNetCore.Localization
+ @{
+ this.HttpContext.Response.Cookies.Append(
+ CookieRequestCultureProvider.DefaultCookieName,
+ CookieRequestCultureProvider.MakeCookieValue(
+ new RequestCulture(
+ CultureInfo.CurrentCulture,
+ CultureInfo.CurrentUICulture)));
+ }

For information on ordering the Localization Middleware in the middleware pipeline of


Program.cs , see ASP.NET Core Middleware.

If the app isn't configured to process controller actions:

Add MVC services by calling AddControllers on the service collection in


Program.cs :
C#

builder.Services.AddControllers();

Add controller endpoint routing in Program.cs by calling MapControllers on the


IEndpointRouteBuilder:

C#

app.MapControllers();

The following example shows the call to UseEndpoints after the line is added:

C#

app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

To provide UI to allow a user to select a culture, use a redirect-based approach with a


localization cookie. The app persists the user's selected culture via a redirect to a
controller. The controller sets the user's selected culture into a cookie and redirects the
user back to the original URI. The process is similar to what happens in a web app when
a user attempts to access a secure resource, where the user is redirected to a sign-in
page and then redirected back to the original resource.

Controllers/CultureController.cs :

C#

using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;

[Route("[controller]/[action]")]
public class CultureController : Controller
{
public IActionResult Set(string culture, string redirectUri)
{
if (culture != null)
{
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture, culture)));
}

return LocalRedirect(redirectUri);
}
}

2 Warning

Use the LocalRedirect action result to prevent open redirect attacks. For more
information, see Prevent open redirect attacks in ASP.NET Core.

The following CultureSelector component shows how to call the Set method of the
CultureController with the new culture. The component is placed in the Shared folder

for use throughout the app.

Shared/CultureSelector.razor :

razor

@using System.Globalization
@inject NavigationManager Navigation

<p>
<label>
Select your locale:
<select @bind="Culture">
@foreach (var culture in supportedCultures)
{
<option value="@culture">@culture.DisplayName</option>
}
</select>
</label>
</p>

@code
{
private CultureInfo[] supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("es-CL"),
};

protected override void OnInitialized()


{
Culture = CultureInfo.CurrentCulture;
}

private CultureInfo Culture


{
get => CultureInfo.CurrentCulture;
set
{
if (CultureInfo.CurrentCulture != value)
{
var uri = new Uri(Navigation.Uri)
.GetComponents(UriComponents.PathAndQuery,
UriFormat.Unescaped);
var cultureEscaped = Uri.EscapeDataString(value.Name);
var uriEscaped = Uri.EscapeDataString(uri);

Navigation.NavigateTo(
$"Culture/Set?culture={cultureEscaped}&redirectUri=
{uriEscaped}",
forceLoad: true);
}
}
}
}

Inside the closing </main> tag in Shared/MainLayout.razor , add the CultureSelector


component:

razor

<article class="bottom-row px-4">


<CultureSelector />
</article>

Use the CultureExample1 component shown in the Demonstration component section


to study how the preceding example works.

Localization
If the app doesn't already support culture selection per the Dynamically set the culture
by user preference section of this article, add the Microsoft.Extensions.Localization
package to the app.

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 .

Use Localization Middleware to set the app's culture.

If the app doesn't already support culture selection per the Dynamically set the culture
by user preference section of this article:
Add localization services to the app with AddLocalization.
Specify the app's default and supported cultures in Program.cs . The following
example configures supported cultures for United States English and Chilean
Spanish.

In Program.cs :

C#

builder.Services.AddLocalization();

In Program.cs immediately after Routing Middleware is added to the processing


pipeline:

C#

var supportedCultures = new[] { "en-US", "es-CL" };


var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);

For information on ordering the Localization Middleware in the middleware pipeline of


Program.cs , see ASP.NET Core Middleware.

If the app should localize resources based on storing a user's culture setting, use a
localization culture cookie. Use of a cookie ensures that the WebSocket connection can
correctly propagate the culture. If localization schemes are based on the URL path or
query string, the scheme might not be able to work with WebSockets, thus fail to persist
the culture. Therefore, the recommended approach is to use a localization culture
cookie. See the Dynamically set the culture by user preference section of this article to
see an example Razor expression that persists the user's culture selection.

The example of localized resources in this section works with the prior examples in this
article where the app's supported cultures are English ( en ) as a default locale and
Spanish ( es ) as a user-selectable or browser-specified alternate locale.

Create resources for each locale. In the following example, resources are created for a
default Greeting string:

English: Hello, World!


Spanish ( es ): ¡Hola, Mundo!
7 Note

The following resource file can be added in Visual Studio by right-clicking the
project's Pages folder and selecting Add > New Item > Resources File. Name the
file CultureExample2.resx . When the editor appears, provide data for a new entry.
Set the Name to Greeting and Value to Hello, World! . Save the file.

Pages/CultureExample2.resx :

XML

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


<root>
<xsd:schema id="root" xmlns=""
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-
microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"
msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0"
msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"
msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string"
msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string"
msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"
msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Greeting" xml:space="preserve">
<value>Hello, World!</value>
</data>
</root>

7 Note

The following resource file can be added in Visual Studio by right-clicking the
project's Pages folder and selecting Add > New Item > Resources File. Name the
file CultureExample2.es.resx . When the editor appears, provide data for a new
entry. Set the Name to Greeting and Value to ¡Hola, Mundo! . Save the file.

Pages/CultureExample2.es.resx :

XML

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


<root>
<xsd:schema id="root" xmlns=""
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-
microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"
msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0"
msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"
msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string"
msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string"
msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"
msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Greeting" xml:space="preserve">
<value>¡Hola, Mundo!</value>
</data>
</root>

The following component demonstrates the use of the localized Greeting string with
IStringLocalizer<T>. The Razor markup @Loc["Greeting"] in the following example
localizes the string keyed to the Greeting value, which is set in the preceding resource
files.

Add the namespace for Microsoft.Extensions.Localization to the app's _Imports.razor


file:

razor

@using Microsoft.Extensions.Localization

Pages/CultureExample2.razor :

razor

@page "/culture-example-2"
@using System.Globalization
@inject IStringLocalizer<CultureExample2> Loc

<h1>Culture Example 2</h1>

<p>
<b>CurrentCulture</b>: @CultureInfo.CurrentCulture
</p>

<h2>Greeting</h2>

<p>
@Loc["Greeting"]
</p>

<p>
@greeting
</p>

@code {
private string? greeting;

protected override void OnInitialized()


{
greeting = Loc["Greeting"];
}
}

Optionally, add a menu item to the navigation in Shared/NavMenu.razor for the


CultureExample2 component.

Shared resources
To create localization shared resources, adopt the following approach.

Create a dummy class with an arbitrary class name. In the following example:
The app uses the BlazorSample namespace, and localization assets use the
BlazorSample.Localization namespace.

The dummy class is named SharedResource .


The class file is placed in a Localization folder at the root of the app.

Localization/SharedResource.cs :

C#

namespace BlazorSample.Localization;

public class SharedResource


{
}

Create the shared resource files with a Build Action of Embedded resource . In the
following example:

The files are placed in the Localization folder with the dummy SharedResource
class ( Localization/SharedResource.cs ).

Name the resource files to match the name of the dummy class. The following
example files include a default localization file and a file for Spanish ( es )
localization.
Localization/SharedResource.resx

Localization/SharedResource.es.resx

7 Note

Localization is resource path that can be set via LocalizationOptions.

To reference the dummy class for an injected IStringLocalizer<T> in a Razor


component, either place an @using directive for the localization namespace or
include the localization namespace in the dummy class reference. In the following
examples:
The first example states the Localization namespace for the SharedResource
dummy class with an @using directive.
The second example states the SharedResource dummy class's namespace
explicitly.

In a Razor component, use either of the following approaches:

razor

@using Localization
@inject IStringLocalizer<SharedResource> Loc

razor

@inject IStringLocalizer<Localization.SharedResource> Loc

For additional guidance, see Globalization and localization in ASP.NET Core.

Additional resources
Set the app base path
Globalization and localization in ASP.NET Core
Globalizing and localizing .NET applications
Resources in .resx Files
Microsoft Multilingual App Toolkit
Localization & Generics
Calling InvokeAsync(StateHasChanged) causes page to fallback to default culture
(dotnet/aspnetcore #28521)
ASP.NET Core Blazor forms and input
components
Article • 06/16/2023

The Blazor framework supports forms and provides built-in input components:

EditForm component bound to a model that uses data annotations


Built-in input components

The Microsoft.AspNetCore.Components.Forms namespace provides:

Classes for managing form elements, state, and validation.


Access to built-in Input* components, which can be used in Blazor apps.

A project created from the Blazor project template includes the namespace by default in
the app's _Imports.razor file, which makes the namespace available in all of the Razor
component files ( .razor ) of the app without explicit @using directives:

razor

@using Microsoft.AspNetCore.Components.Forms

To demonstrate how an EditForm component works, consider the following example.


ExampleModel represents the data model bound to the form and defines a Name
property, which is used to store the value of the form's name field provided by the user.

ExampleModel.cs :

C#

public class ExampleModel


{
public string? Name { get; set; }
}

A form is defined using the Blazor framework's EditForm component. The following
Razor component demonstrates typical elements, components, and Razor code to
render a webform using an EditForm component, which is bound to the preceding
ExampleModel type.

Pages/FormExample1.razor :
razor

@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<EditForm Model="@exampleModel" OnSubmit="@HandleSubmit">


<InputText @bind-Value="exampleModel.Name" />

<button type="submit">Submit</button>
</EditForm>

@code {
private ExampleModel exampleModel = new();

private void HandleSubmit()


{
Logger.LogInformation("HandleSubmit called");

// Process the form


}
}

In the preceding FormExample1 component:

The EditForm component is rendered where the <EditForm> element appears.


The model is created in the component's @code block and held in a private field
( exampleModel ). The field is assigned to EditForm.Model's attribute ( Model ) of the
<EditForm> element.
The InputText component is an input component for editing string values. The
@bind-Value directive attribute binds the exampleModel.Name model property to the
InputText component's Value property.
The HandleSubmit method is registered as a handler for the OnSubmit callback.
The handler is called when the form is submitted by the user.

To demonstrate how the preceding EditForm component works with data annotations
validation:

The preceding ExampleModel uses the System.ComponentModel.DataAnnotations


namespace.
The Name property of ExampleModel is marked required with the RequiredAttribute
and specifies a StringLengthAttribute maximum string length limit and error
message.

ExampleModel.cs :

C#
using System.ComponentModel.DataAnnotations;

public class ExampleModel


{
[Required]
[StringLength(10, ErrorMessage = "Name is too long.")]
public string? Name { get; set; }
}

The earlier FormExample1 component is modified:

OnSubmit is replaced with OnValidSubmit, which processes assigned event handler


if the form is valid when submitted by the user. The method name is changed to
HandleValidSubmit , which reflects that the method is called when the form is valid.
A ValidationSummary component is added to display validation messages when
the form is invalid on form submission.
The data annotations validator (DataAnnotationsValidator component†) attaches
validation support using data annotations:
If the <input> form field is left blank when the Submit button is selected, an
error appears in the validation summary (ValidationSummary component‡) (" The
Name field is required. ") and HandleValidSubmit is not called.
If the <input> form field contains more than ten characters when the Submit
button is selected, an error appears in the validation summary (" Name is too
long. ") and HandleValidSubmit is not called.
If the <input> form field contains a valid value when the Submit button is
selected, HandleValidSubmit is called.

†The DataAnnotationsValidator component is covered in the Validator component


section. ‡The ValidationSummary component is covered in the Validation Summary and
Validation Message components section. For more information on property binding, see
ASP.NET Core Blazor data binding.

Pages/FormExample1.razor :

razor

@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">


<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="exampleModel.Name" />

<button type="submit">Submit</button>
</EditForm>

@code {
private ExampleModel exampleModel = new();

private void HandleValidSubmit()


{
Logger.LogInformation("HandleValidSubmit called");

// Process the valid form


}
}

Binding a form
An EditForm creates an EditContext based on the assigned model instance as a
cascading value for other components in the form. The EditContext tracks metadata
about the edit process, including which fields have been modified and the current
validation messages. Assigning to either an EditForm.Model or an EditForm.EditContext
can bind a form to data.

Assignment to EditForm.Model:

razor

<EditForm Model="@exampleModel" ...>

@code {
private ExampleModel exampleModel = new() { ... };
}

Assignment to EditForm.EditContext:

razor

<EditForm EditContext="@editContext" ...>

@code {
private ExampleModel exampleModel = new() { ... };
private EditContext? editContext;

protected override void OnInitialized()


{
editContext = new(exampleModel);
}
}

Assign either an EditContext or a Model to an EditForm. Assignment of both isn't


supported and generates a runtime error:

Unhandled exception rendering component: EditForm requires a Model parameter,


or an EditContext parameter, but not both.

Handle form submission


The EditForm provides the following callbacks for handling form submission:

Use OnValidSubmit to assign an event handler to run when a form with valid fields
is submitted.
Use OnInvalidSubmit to assign an event handler to run when a form with invalid
fields is submitted.
Use OnSubmit to assign an event handler to run regardless of the form fields'
validation status. The form is validated by calling EditContext.Validate in the event
handler method. If Validate returns true , the form is valid.

Built-in input components


The Blazor framework provides built-in input components to receive and validate user
input. The built-in input components in the following table are supported in an EditForm
with an EditContext.

The components in the table are also supported outside of a form in Razor component
markup. Inputs are validated when they're changed and when a form is submitted.

Input component Rendered as…

InputCheckbox <input type="checkbox">

InputDate<TValue> <input type="date">

InputFile <input type="file">

InputNumber<TValue> <input type="number">

InputRadio<TValue> <input type="radio">

InputRadioGroup<TValue> Group of child InputRadio<TValue>


Input component Rendered as…

InputSelect<TValue> <select>

InputText <input>

InputTextArea <textarea>

For more information on the InputFile component, see ASP.NET Core Blazor file uploads.

All of the input components, including EditForm, support arbitrary attributes. Any
attribute that doesn't match a component parameter is added to the rendered HTML
element.

Input components provide default behavior for validating when a field is changed:

For input components in a form with an EditContext, the default validation


behavior includes updating the field CSS class to reflect the field's state as valid or
invalid with validation styling of the underlying HTML element.
For controls that don't have an EditContext, the default validation reflects the valid
or invalid state but does not provide validation styling to the underlying HTML
element.

Some components include useful parsing logic. For example, InputDate<TValue> and
InputNumber<TValue> handle unparseable values gracefully by registering unparseable
values as validation errors. Types that can accept null values also support nullability of
the target field (for example, int? for a nullable integer).

For more information on the InputFile component, see ASP.NET Core Blazor file uploads.

Example form
The following Starship type, which is used in several of this article's examples, defines a
diverse set of properties with data annotations:

Identifier is required because it's annotated with the RequiredAttribute.

Identifier requires a value of at least one character but no more than 16

characters using the StringLengthAttribute.


Description is optional because it isn't annotated with the RequiredAttribute.

Classification is required.
The MaximumAccommodation property defaults to zero but requires a value from one
to 100,000 per its RangeAttribute.
IsValidatedDesign requires that the property have a true value, which matches a

selected state when the property is bound to a checkbox in the UI ( <input


type="checkbox"> ).

ProductionDate is a DateTime and required.

Starship.cs :

C#

using System.ComponentModel.DataAnnotations;

public class Starship


{
[Required]
[StringLength(16, ErrorMessage = "Identifier too long (16 character
limit).")]
public string? Identifier { get; set; }

public string? Description { get; set; }

[Required]
public string? Classification { get; set; }

[Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]


public int MaximumAccommodation { get; set; }

[Required]
[Range(typeof(bool), "true", "true",
ErrorMessage = "This form disallows unapproved ships.")]
public bool IsValidatedDesign { get; set; }

[Required]
public DateTime ProductionDate { get; set; }
}

The following form accepts and validates user input using:

The properties and validation defined in the preceding Starship model.


Several of Blazor's built-in input components.

Pages/FormExample2.razor :

razor

@page "/form-example-2"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample2> Logger

<h1>Starfleet Starship Database</h1>


<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">


<DataAnnotationsValidator />
<ValidationSummary />

<p>
<label>
Identifier:
<InputText @bind-Value="starship.Identifier" />
</label>
</p>
<p>
<label>
Description (optional):
<InputTextArea @bind-Value="starship.Description" />
</label>
</p>
<p>
<label>
Primary Classification:
<InputSelect @bind-Value="starship.Classification">
<option value="">Select classification ...</option>
<option value="Exploration">Exploration</option>
<option value="Diplomacy">Diplomacy</option>
<option value="Defense">Defense</option>
</InputSelect>
</label>
</p>
<p>
<label>
Maximum Accommodation:
<InputNumber @bind-Value="starship.MaximumAccommodation" />
</label>
</p>
<p>
<label>
Engineering Approval:
<InputCheckbox @bind-Value="starship.IsValidatedDesign" />
</label>
</p>
<p>
<label>
Production Date:
<InputDate @bind-Value="starship.ProductionDate" />
</label>
</p>

<button type="submit">Submit</button>

<p>
<a href="http://www.startrek.com/">Star Trek</a>,
©1966-2019 CBS Studios, Inc. and
<a href="https://www.paramount.com">Paramount Pictures</a>
</p>
</EditForm>

@code {
private Starship starship = new() { ProductionDate = DateTime.UtcNow };

private void HandleValidSubmit()


{
Logger.LogInformation("HandleValidSubmit called");

// Process the valid form


}
}

The EditForm in the preceding example creates an EditContext based on the assigned
Starship instance ( Model="@starship" ) and handles a valid form. The next example

( FormExample3 component) demonstrates how to assign an EditContext to a form and


validate when the form is submitted.

In the following example:

A shortened version of the preceding Starfleet Starship Database form


( FormExample2 component) is used that only accepts a value for the starship's
identifier. The other Starship properties receive valid default values when an
instance of the Starship type is created.
The HandleSubmit method executes when the Submit button is selected.
The form is validated by calling EditContext.Validate in the HandleSubmit method.
Logging is executed depending on the validation result.

7 Note

HandleSubmit in the FormExample3 component is demonstrated as an asynchronous


method because storing form values often uses asynchronous calls ( await ... ). If
the form is used in a test app as shown, HandleSubmit merely runs synchronously.
For testing purposes, ignore the following build warning:

This async method lacks 'await' operators and will run synchronously. ...

Pages/FormExample3.razor :

razor

@page "/form-example-3"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample3> Logger

<EditForm EditContext="@editContext" OnSubmit="@HandleSubmit">


<DataAnnotationsValidator />
<ValidationSummary />

<p>
<label>
Identifier:
<InputText @bind-Value="starship.Identifier" />
</label>
</p>

<button type="submit">Submit</button>

<p>
<a href="http://www.startrek.com/">Star Trek</a>,
©1966-2019 CBS Studios, Inc. and
<a href="https://www.paramount.com">Paramount Pictures</a>
</p>
</EditForm>

@code {
private Starship starship =
new()
{
Identifier = "NCC-1701",
Classification = "Exploration",
MaximumAccommodation = 150,
IsValidatedDesign = true,
ProductionDate = new DateTime(2245, 4, 11)
};
private EditContext? editContext;

protected override void OnInitialized()


{
editContext = new(starship);
}

private async Task HandleSubmit()


{
if (editContext != null && editContext.Validate())
{
Logger.LogInformation("HandleSubmit called: Form is valid");

// Process the valid form


// await ...
await Task.CompletedTask;
}
else
{
Logger.LogInformation("HandleSubmit called: Form is INVALID");
}
}
}
7 Note

Changing the EditContext after it's assigned is not supported.

Multiple option selection with the InputSelect


component
Binding supports multiple option selection with the InputSelect<TValue> component.
The @onchange event provides an array of the selected options via event arguments
(ChangeEventArgs). The value must be bound to an array type, and binding to an array
type makes the multiple attribute optional on the InputSelect<TValue> tag.

In the following example, the user must select at least two starship classifications but no
more than three classifications.

Pages/BindMultipleWithInputSelect.razor :

razor

@page "/bind-multiple-with-inputselect"
@using System.ComponentModel.DataAnnotations
@using Microsoft.Extensions.Logging
@inject ILogger<BindMultipleWithInputSelect> Logger

<h1>Bind Multiple <code>InputSelect</code>Example</h1>

<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">


<DataAnnotationsValidator />
<ValidationSummary />

<p>
<label>
Select classifications (Minimum: 2, Maximum: 3):
<InputSelect @bind-Value="starship.SelectedClassification">
<option
value="@Classification.Exploration">Exploration</option>
<option value="@Classification.Diplomacy">Diplomacy</option>
<option value="@Classification.Defense">Defense</option>
<option value="@Classification.Research">Research</option>
</InputSelect>
</label>
</p>

<button type="submit">Submit</button>
</EditForm>
<p>
Selected Classifications:
@string.Join(", ", starship.SelectedClassification)
</p>

@code {
private EditContext? editContext;
private Starship starship = new();

protected override void OnInitialized()


{
editContext = new(starship);
}

private void HandleValidSubmit()


{
Logger.LogInformation("HandleValidSubmit called");
}

private class Starship


{
[Required, MinLength(2), MaxLength(3)]
public Classification[] SelectedClassification { get; set; } =
new[] { Classification.Diplomacy };
}

private enum Classification { Exploration, Diplomacy, Defense, Research


}
}

For information on how empty strings and null values are handled in data binding, see
the Binding InputSelect options to C# object null values section.

Binding InputSelect options to C# object null


values
For information on how empty strings and null values are handled in data binding, see
ASP.NET Core Blazor data binding.

Display name support


Several built-in components support display names with the
InputBase<TValue>.DisplayName parameter.

In the Starfleet Starship Database form ( FormExample2 component) of the Example


form section, the production date of a new starship doesn't specify a display name:
razor

<label>
Production Date:
<InputDate @bind-Value="starship.ProductionDate" />
</label>

If the field contains an invalid date when the form is submitted, the error message
doesn't display a friendly name. The field name, " ProductionDate " doesn't have a space
between " Production " and " Date " when it appears in the validation summary:

The ProductionDate field must be a date.

Set the DisplayName property to a friendly name with a space between the words
" Production " and " Date ":

razor

<label>
Production Date:
<InputDate @bind-Value="starship.ProductionDate"
DisplayName="Production Date" />
</label>

The validation summary displays the friendly name when the field's value is invalid:

The Production Date field must be a date.

Error message template support


InputDate<TValue> and InputNumber<TValue> support error message templates:

InputDate<TValue>.ParsingErrorMessage
InputNumber<TValue>.ParsingErrorMessage

In the Starfleet Starship Database form ( FormExample2 component) of the Example


form section with a friendly display name assigned, the Production Date field produces
an error message using the following default error message template:

css

The {0} field must be a date.


The position of the {0} placeholder is where the value of the DisplayName property
appears when the error is displayed to the user.

razor

<label>
Production Date:
<InputDate @bind-Value="starship.ProductionDate"
DisplayName="Production Date" />
</label>

The Production Date field must be a date.

Assign a custom template to ParsingErrorMessage to provide a custom message:

razor

<label>
Production Date:
<InputDate @bind-Value="starship.ProductionDate"
DisplayName="Production Date"
ParsingErrorMessage="The {0} field has an incorrect date
value." />
</label>

The Production Date field has an incorrect date value.

Basic validation
In basic form validation scenarios, an EditForm instance can use declared EditContext
and ValidationMessageStore instances to validate form fields. A handler for the
OnValidationRequested event of the EditContext executes custom validation logic. The
handler's result updates the ValidationMessageStore instance.

Basic form validation is useful in cases where the form's model is defined within the
component hosting the form, either as members directly on the component or in a
subclass. Use of a validator component is recommended where an independent model
class is used across several components.

In the following FormExample4 component, the HandleValidationRequested handler


method clears any existing validation messages by calling ValidationMessageStore.Clear
before validating the form.

Pages/FormExample4.razor :
razor

@page "/form-example-4"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample4> Logger

<h2>Ship Holodecks</h2>

<EditForm EditContext="editContext" OnValidSubmit="@HandleValidSubmit">


<label>
Type 1:
<InputCheckbox @bind-Value="holodeck.Type1" />
</label>

<label>
Type 2:
<InputCheckbox @bind-Value="holodeck.Type2" />
</label>

<button type="submit">Update</button>

<ValidationMessage For="() => holodeck.Options" />

<p>
<a href="http://www.startrek.com/">Star Trek</a>,
©1966-2019 CBS Studios, Inc. and
<a href="https://www.paramount.com">Paramount Pictures</a>
</p>
</EditForm>

@code {
private EditContext? editContext;
private Holodeck holodeck = new();
private ValidationMessageStore? messageStore;

protected override void OnInitialized()


{
editContext = new(holodeck);
editContext.OnValidationRequested += HandleValidationRequested;
messageStore = new(editContext);
}
private void HandleValidationRequested(object? sender,
ValidationRequestedEventArgs args)
{
messageStore?.Clear();

// Custom validation logic


if (!holodeck.Options)
{
messageStore?.Add(() => holodeck.Options, "Select at least
one.");
}
}

private void HandleValidSubmit()


{
Logger.LogInformation("HandleValidSubmit called: Processing the
form");

// Process the form


}

public class Holodeck


{
public bool Type1 { get; set; }
public bool Type2 { get; set; }
public bool Options => Type1 || Type2;
}

public void Dispose()


{
if (editContext is not null)
{
editContext.OnValidationRequested -= HandleValidationRequested;
}
}
}

Data Annotations Validator component and


custom validation
The DataAnnotationsValidator component attaches data annotations validation to a
cascaded EditContext. Enabling data annotations validation requires the
DataAnnotationsValidator component. To use a different validation system than data
annotations, use a custom implementation instead of the DataAnnotationsValidator
component. The framework implementations for DataAnnotationsValidator are available
for inspection in the reference source:

DataAnnotationsValidator
AddDataAnnotationsValidation .
7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Blazor performs two types of validation:

Field validation is performed when the user tabs out of a field. During field
validation, the DataAnnotationsValidator component associates all reported
validation results with the field.
Model validation is performed when the user submits the form. During model
validation, the DataAnnotationsValidator component attempts to determine the
field based on the member name that the validation result reports. Validation
results that aren't associated with an individual member are associated with the
model rather than a field.

Validator components
Validator components support form validation by managing a ValidationMessageStore
for a form's EditContext.

The Blazor framework provides the DataAnnotationsValidator component to attach


validation support to forms based on validation attributes (data annotations). You can
create custom validator components to process validation messages for different forms
on the same page or the same form at different steps of form processing (for example,
client-side validation followed by server-side validation). The validator component
example shown in this section, CustomValidation , is used in the following sections of
this article:

Business logic validation with a validator component


Server validation with a validator component

7 Note

Custom data annotation validation attributes can be used instead of custom


validator components in many cases. Custom attributes applied to the form's
model activate with the use of the DataAnnotationsValidator component. When
used with server-side validation, any custom attributes applied to the model must
be executable on the server. For more information, see Model validation in
ASP.NET Core MVC.

Create a validator component from ComponentBase:

The form's EditContext is a cascading parameter of the component.


When the validator component is initialized, a new ValidationMessageStore is
created to maintain a current list of form errors.
The message store receives errors when developer code in the form's component
calls the DisplayErrors method. The errors are passed to the DisplayErrors
method in a Dictionary<string, List<string>>. In the dictionary, the key is the name
of the form field that has one or more errors. The value is the error list.
Messages are cleared when any of the following have occurred:
Validation is requested on the EditContext when the OnValidationRequested
event is raised. All of the errors are cleared.
A field changes in the form when the OnFieldChanged event is raised. Only the
errors for the field are cleared.
The ClearErrors method is called by developer code. All of the errors are
cleared.

CustomValidation.cs (if used in a test app, change the namespace, BlazorSample , to

match the app's namespace):

C#

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample;

public class CustomValidation : ComponentBase


{
private ValidationMessageStore? messageStore;

[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }

protected override void OnInitialized()


{
if (CurrentEditContext is null)
{
throw new InvalidOperationException(
$"{nameof(CustomValidation)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. " +
$"For example, you can use {nameof(CustomValidation)} " +
$"inside an {nameof(EditForm)}.");
}
messageStore = new(CurrentEditContext);

CurrentEditContext.OnValidationRequested += (s, e) =>


messageStore?.Clear();
CurrentEditContext.OnFieldChanged += (s, e) =>
messageStore?.Clear(e.FieldIdentifier);
}

public void DisplayErrors(Dictionary<string, List<string>> errors)


{
if (CurrentEditContext is not null)
{
foreach (var err in errors)
{
messageStore?.Add(CurrentEditContext.Field(err.Key),
err.Value);
}

CurrentEditContext.NotifyValidationStateChanged();
}
}

public void ClearErrors()


{
messageStore?.Clear();
CurrentEditContext?.NotifyValidationStateChanged();
}
}

) Important

Specifying a namespace is required when deriving from ComponentBase. Failing to


specify a namespace results in a build error:

Tag helpers cannot target tag name '<global namespace>.{CLASS NAME}'


because it contains a ' ' character.

The {CLASS NAME} placeholder is the name of the component class. The custom
validator example in this section specifies the example namespace BlazorSample .

7 Note

Anonymous lambda expressions are registered event handlers for


OnValidationRequested and OnFieldChanged in the preceding example. It isn't
necessary to implement IDisposable and unsubscribe the event delegates in this
scenario. For more information, see ASP.NET Core Razor component lifecycle.
Business logic validation with a validator
component
For general business logic validation, use a validator component that receives form
errors in a dictionary.

Basic validation is useful in cases where the form's model is defined within the
component hosting the form, either as members directly on the component or in a
subclass. Use of a validator component is recommended where an independent model
class is used across several components.

In the following example:

A shortened version of the Starfleet Starship Database form ( FormExample2


component) from the Example form section is used that only accepts the starship's
classification and description. Data annotation validation is not triggered on form
submission because the DataAnnotationsValidator component isn't included in the
form.
The CustomValidation component from the Validator components section of this
article is used.
The validation requires a value for the ship's description ( Description ) if the user
selects the " Defense " ship classification ( Classification ).

When validation messages are set in the component, they're added to the validator's
ValidationMessageStore and shown in the EditForm's validation summary.

Pages/FormExample5.razor :

razor

@page "/form-example-5"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample5> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">


<CustomValidation @ref="customValidation" />
<ValidationSummary />

<p>
<label>
Primary Classification:
<InputSelect @bind-Value="starship.Classification">
<option value="">Select classification ...</option>
<option value="Exploration">Exploration</option>
<option value="Diplomacy">Diplomacy</option>
<option value="Defense">Defense</option>
</InputSelect>
</label>
</p>
<p>
<label>
Description (optional):
<InputTextArea @bind-Value="starship.Description" />
</label>
</p>

<button type="submit">Submit</button>

<p>
<a href="http://www.startrek.com/">Star Trek</a>,
©1966-2019 CBS Studios, Inc. and
<a href="https://www.paramount.com">Paramount Pictures</a>
</p>
</EditForm>

@code {
private CustomValidation? customValidation;
private Starship starship = new() { ProductionDate = DateTime.UtcNow };

private void HandleValidSubmit()


{
customValidation?.ClearErrors();

var errors = new Dictionary<string, List<string>>();

if (starship.Classification == "Defense" &&


string.IsNullOrEmpty(starship.Description))
{
errors.Add(nameof(starship.Description),
new() { "For a 'Defense' ship classification, " +
"'Description' is required." });
}

if (errors.Any())
{
customValidation?.DisplayErrors(errors);
}
else
{
Logger.LogInformation("HandleValidSubmit called: Processing the
form");

// Process the valid form


}
}
}

7 Note

As an alternative to using validation components, data annotation validation


attributes can be used. Custom attributes applied to the form's model activate with
the use of the DataAnnotationsValidator component. When used with server-side
validation, the attributes must be executable on the server. For more information,
see Model validation in ASP.NET Core MVC.

Server validation with a validator component


Server validation is supported in addition to client-side validation:

Process client-side validation in the form with the DataAnnotationsValidator


component.
When the form passes client-side validation (OnValidSubmit is called), send the
EditContext.Model to a backend server API for form processing.
Process model validation on the server.
The server API includes both the built-in framework data annotations validation
and custom validation logic supplied by the developer. If validation passes on the
server, process the form and send back a success status code (200 - OK ). If
validation fails, return a failure status code (400 - Bad Request ) and the field
validation errors.
Either disable the form on success or display the errors.

Basic validation is useful in cases where the form's model is defined within the
component hosting the form, either as members directly on the component or in a
subclass. Use of a validator component is recommended where an independent model
class is used across several components.

The following example is based on:

A hosted Blazor WebAssembly solution created from the Blazor WebAssembly


project template. The approach is supported for any of the secure hosted Blazor
solutions described in the hosted Blazor WebAssembly security documentation.
The Starship model ( Starship.cs ) from the Example form section.
The CustomValidation component shown in the Validator components section.
Place the Starship model ( Starship.cs ) into the solution's Shared project so that both
the client and server apps can use the model. Add or update the namespace to match
the namespace of the shared app (for example, namespace BlazorSample.Shared ). Since
the model requires data annotations, add the System.ComponentModel.Annotations
package to the Shared project.

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 .

In the Server project, add a controller to process starship validation requests and return
failed validation messages. Update the namespaces in the last using statement for the
Shared project and the namespace for the controller class. In addition to data
annotations validation (client-side and server-side), the controller validates that a value
is provided for the ship's description ( Description ) if the user selects the Defense ship
classification ( Classification ).

The validation for the Defense ship classification only occurs server-side in the controller
because the upcoming form doesn't perform the same validation client-side when the
form is submitted to the server. Server-side validation without client-side validation is
common in apps that require private business logic validation of user input on the
server. For example, private information from data stored for a user might be required
to validate user input. Private data obviously can't be sent to the client for client-side
validation.

7 Note

The StarshipValidation controller in this section uses Microsoft Identity 2.0. The
Web API only accepts tokens for users that have the " API.Access " scope for this
API. Additional customization is required if the API's scope name is different from
API.Access . For a version of the controller that works with Microsoft Identity 1.0

and ASP.NET Core prior to version 5.0, see an earlier version of this article.

For more information on security, see:

ASP.NET Core Blazor authentication and authorization (and the other articles
in the Blazor Security and Identity node)
Microsoft identity platform documentation
Controllers/StarshipValidation.cs :

C#

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web.Resource;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
public class StarshipValidationController : ControllerBase
{
private readonly ILogger<StarshipValidationController> logger;

public StarshipValidationController(
ILogger<StarshipValidationController> logger)
{
this.logger = logger;
}

static readonly string[] scopeRequiredByApi = new[] { "API.Access" };

[HttpPost]
public async Task<IActionResult> Post(Starship starship)
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

try
{
if (starship.Classification == "Defense" &&
string.IsNullOrEmpty(starship.Description))
{
ModelState.AddModelError(nameof(starship.Description),
"For a 'Defense' ship " +
"classification, 'Description' is required.");
}
else
{
logger.LogInformation("Processing the form asynchronously");

// Process the valid form


// async ...

return Ok(ModelState);
}
}
catch (Exception ex)
{
logger.LogError("Validation Error: {Message}", ex.Message);
}

return BadRequest(ModelState);
}
}

If using the preceding controller in a hosted Blazor WebAssembly app, update the
namespace ( BlazorSample.Server.Controllers ) to match the app's controllers
namespace.

When a model binding validation error occurs on the server, an ApiController


(ApiControllerAttribute) normally returns a default bad request response with a
ValidationProblemDetails. The response contains more data than just the validation
errors, as shown in the following example when all of the fields of the Starfleet
Starship Database form aren't submitted and the form fails validation:

JSON

{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Identifier": ["The Identifier field is required."],
"Classification": ["The Classification field is required."],
"IsValidatedDesign": ["This form disallows unapproved ships."],
"MaximumAccommodation": ["Accommodation invalid (1-100000)."]
}
}

7 Note

To demonstrate the preceding JSON response, you must either disable the form's
client-side validation to permit empty field form submission or use a tool to send a
request directly to the server API, such as Firefox Browser Developer or
Postman .

If the server API returns the preceding default JSON response, it's possible for the client
to parse the response in developer code to obtain the children of the errors node for
forms validation error processing. It's inconvenient to write developer code to parse the
file. Parsing the JSON manually requires producing a Dictionary<string, List<string>> of
errors after calling ReadFromJsonAsync. Ideally, the server API should only return the
validation errors:
JSON

{
"Identifier": ["The Identifier field is required."],
"Classification": ["The Classification field is required."],
"IsValidatedDesign": ["This form disallows unapproved ships."],
"MaximumAccommodation": ["Accommodation invalid (1-100000)."]
}

To modify the server API's response to make it only return the validation errors, change
the delegate that's invoked on actions that are annotated with ApiControllerAttribute in
Program.cs . For the API endpoint ( /StarshipValidation ), return a

BadRequestObjectResult with the ModelStateDictionary. For any other API endpoints,


preserve the default behavior by returning the object result with a new
ValidationProblemDetails.

Add the Microsoft.AspNetCore.Mvc namespace to the top of the Program.cs file in the
Server app:

C#

using Microsoft.AspNetCore.Mvc;

In Program.cs , locate the AddControllersWithViews extension method and add the


following call to ConfigureApiBehaviorOptions:

C#

builder.Services.AddControllersWithViews()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
if (context.HttpContext.Request.Path == "/StarshipValidation")
{
return new BadRequestObjectResult(context.ModelState);
}
else
{
return new BadRequestObjectResult(
new ValidationProblemDetails(context.ModelState));
}
};
});

For more information, see Handle errors in ASP.NET Core web APIs.
In the Client project, add the CustomValidation component shown in the Validator
components section. Update the namespace to match the app (for example, namespace
BlazorSample.Client ).

In the Client project, the Starfleet Starship Database form is updated to show server
validation errors with help of the CustomValidation component. When the server API
returns validation messages, they're added to the CustomValidation component's
ValidationMessageStore. The errors are available in the form's EditContext for display by
the form's validation summary.

In the following FormExample6 component, update the namespace of the Shared project
( @using BlazorSample.Shared ) to the shared project's namespace. Note that the form
requires authorization, so the user must be signed into the app to navigate to the form.

Pages/FormExample6.razor :

razor

@page "/form-example-6"
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Logging
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<FormExample6> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">


<DataAnnotationsValidator />
<CustomValidation @ref="customValidation" />
<ValidationSummary />

<p>
<label>
Identifier:
<InputText @bind-Value="starship.Identifier"
disabled="@disabled" />
</label>
</p>
<p>
<label>
Description (optional):
<InputTextArea @bind-Value="starship.Description"
disabled="@disabled" />
</label>
</p>
<p>
<label>
Primary Classification:
<InputSelect @bind-Value="starship.Classification"
disabled="@disabled">
<option value="">Select classification ...</option>
<option value="Exploration">Exploration</option>
<option value="Diplomacy">Diplomacy</option>
<option value="Defense">Defense</option>
</InputSelect>
</label>
</p>
<p>
<label>
Maximum Accommodation:
<InputNumber @bind-Value="starship.MaximumAccommodation"
disabled="@disabled" />
</label>
</p>
<p>
<label>
Engineering Approval:
<InputCheckbox @bind-Value="starship.IsValidatedDesign"
disabled="@disabled" />
</label>
</p>
<p>
<label>
Production Date:
<InputDate @bind-Value="starship.ProductionDate"
disabled="@disabled" />
</label>
</p>

<button type="submit" disabled="@disabled">Submit</button>

<p style="@messageStyles">
@message
</p>

<p>
<a href="http://www.startrek.com/">Star Trek</a>,
&copy;1966-2019 CBS Studios, Inc. and
<a href="https://www.paramount.com">Paramount Pictures</a>
</p>
</EditForm>

@code {
private bool disabled;
private string? message;
private string? messageStyles = "visibility:hidden";
private CustomValidation? customValidation;
private Starship starship = new() { ProductionDate = DateTime.UtcNow };
private async Task HandleValidSubmit(EditContext editContext)
{
customValidation?.ClearErrors();

try
{
var response = await Http.PostAsJsonAsync<Starship>(
"StarshipValidation", (Starship)editContext.Model);

var errors = await response.Content


.ReadFromJsonAsync<Dictionary<string, List<string>>>() ??
new Dictionary<string, List<string>>();

if (response.StatusCode == HttpStatusCode.BadRequest &&


errors.Any())
{
customValidation?.DisplayErrors(errors);
}
else if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Validation failed. Status Code:
{response.StatusCode}");
}
else
{
disabled = true;
messageStyles = "color:green";
message = "The form has been processed.";
}
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
catch (Exception ex)
{
Logger.LogError("Form processing error: {Message}", ex.Message);
disabled = true;
messageStyles = "color:red";
message = "There was an error processing the form.";
}
}
}

7 Note

As an alternative to the use of a validation component, data annotation validation


attributes can be used. Custom attributes applied to the form's model activate with
the use of the DataAnnotationsValidator component. When used with server-side
validation, the attributes must be executable on the server. For more information,
see Model validation in ASP.NET Core MVC.

7 Note

The server-side validation approach in this section is suitable for any of the hosted
Blazor WebAssembly solution examples in this documentation set:

Azure Active Directory (AAD)


Azure Active Directory (AAD) B2C
Identity Server

InputText based on the input event


Use the InputText component to create a custom component that uses the oninput
event (input ) instead of the onchange event (change ). Use of the input event
triggers field validation on each keystroke.

The following example uses the ExampleModel class.

ExampleModel.cs :

C#

using System.ComponentModel.DataAnnotations;

public class ExampleModel


{
[Required]
[StringLength(10, ErrorMessage = "Name is too long.")]
public string? Name { get; set; }
}

The following CustomInputText component inherits the framework's InputText


component and sets event binding to the oninput event (input ).

Shared/CustomInputText.razor :

razor

@inherits InputText

<input @attributes="AdditionalAttributes"
class="@CssClass"
@bind="CurrentValueAsString"
@bind:event="oninput" />

The CustomInputText component can be used anywhere InputText is used. The following
FormExample7 component uses the shared CustomInputText component.

Pages/FormExample7.razor :

razor

@page "/form-example-7"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample7> Logger

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">


<DataAnnotationsValidator />
<ValidationSummary />

<CustomInputText @bind-Value="exampleModel.Name" />

<button type="submit">Submit</button>
</EditForm>

<p>
CurrentValue: @exampleModel.Name
</p>

@code {
private ExampleModel exampleModel = new();

private void HandleValidSubmit()


{
Logger.LogInformation("HandleValidSubmit called");

// Process the valid form


}
}

Radio buttons
The example in this section is based on the Starfleet Starship Database form of the
Example form section of this article.

Add the following enum types to the app. Create a new file to hold them or add them to
the Starship.cs file.

C#
public class ComponentEnums
{
public enum Manufacturer { SpaceX, NASA, ULA, VirginGalactic, Unknown }
public enum Color { ImperialRed, SpacecruiserGreen, StarshipBlue,
VoyagerOrange }
public enum Engine { Ion, Plasma, Fusion, Warp }
}

Make the enums accessible to the:

Starship model in Starship.cs (for example, using static ComponentEnums; if the


enums class is named ComponentEnums ).

Starfleet Starship Database form (for example, @using static ComponentEnums if


the enums class is named ComponentEnums ).

Use InputRadio<TValue> components with the InputRadioGroup<TValue> component


to create a radio button group. In the following example, properties are added to the
Starship model described in the Example form section:

C#

[Required]
[Range(typeof(Manufacturer), nameof(Manufacturer.SpaceX),
nameof(Manufacturer.VirginGalactic), ErrorMessage = "Pick a
manufacturer.")]
public Manufacturer Manufacturer { get; set; } = Manufacturer.Unknown;

[Required, EnumDataType(typeof(Color))]
public Color? Color { get; set; } = null;

[Required, EnumDataType(typeof(Engine))]
public Engine? Engine { get; set; } = null;

Update the Starfleet Starship Database form ( FormExample2 component) from the
Example form section. Add the components to produce:

A radio button group for the ship manufacturer.


A nested radio button group for engine and ship color.

7 Note

Nested radio button groups aren't often used in forms because they can result in a
disorganized layout of form controls that may confuse users. However, there are
cases when they make sense in UI design, such as in the following example that
pairs recommendations for two user inputs, ship engine and ship color. One engine
and one color are required by the form's validation. The form's layout uses nested
InputRadioGroup<TValue>s to pair engine and color recommendations. However,
the user can combine any engine with any color to submit the form.

razor

<fieldset>
<legend>Manufacturer</legend>
<InputRadioGroup @bind-Value="starship.Manufacturer">
@foreach (var manufacturer in (Manufacturer[])Enum
.GetValues(typeof(Manufacturer)))
{
<label>
<InputRadio Value="@manufacturer" />
<text>&nbsp;</text>@manufacturer
</label>
}
</InputRadioGroup>
<fieldset>

<p>
Select one engine and one color. Recommendations are paired but any
combination of engine and color is allowed:<br>
<InputRadioGroup Name="engine" @bind-Value="starship.Engine">
<InputRadioGroup Name="color" @bind-Value="starship.Color">
<InputRadio Name="engine" Value="@Engine.Ion" />
Engine: Ion<br>
<InputRadio Name="color" Value="@Color.ImperialRed" />
Color: Imperial Red<br><br>
<InputRadio Name="engine" Value="@Engine.Plasma" />
Engine: Plasma<br>
<InputRadio Name="color" Value="@Color.SpacecruiserGreen" />
Color: Spacecruiser Green<br><br>
<InputRadio Name="engine" Value="@Engine.Fusion" />
Engine: Fusion<br>
<InputRadio Name="color" Value="@Color.StarshipBlue" />
Color: Starship Blue<br><br>
<InputRadio Name="engine" Value="@Engine.Warp" />
Engine: Warp<br>
<InputRadio Name="color" Value="@Color.VoyagerOrange" />
Color: Voyager Orange
</InputRadioGroup>
</InputRadioGroup>
</p>

7 Note

If Name is omitted, InputRadio<TValue> components are grouped by their most


recent ancestor.
Validation Summary and Validation Message
components
The ValidationSummary component summarizes all validation messages, which is similar
to the Validation Summary Tag Helper:

razor

<ValidationSummary />

Output validation messages for a specific model with the Model parameter:

razor

<ValidationSummary Model="@starship" />

The ValidationMessage<TValue> component displays validation messages for a specific


field, which is similar to the Validation Message Tag Helper. Specify the field for
validation with the For attribute and a lambda expression naming the model property:

razor

<ValidationMessage For="@(() => starship.MaximumAccommodation)" />

The ValidationMessage<TValue> and ValidationSummary components support arbitrary


attributes. Any attribute that doesn't match a component parameter is added to the
generated <div> or <ul> element.

Control the style of validation messages in the app's stylesheet ( wwwroot/css/app.css or


wwwroot/css/site.css ). The default validation-message class sets the text color of
validation messages to red:

css

.validation-message {
color: red;
}

Custom validation attributes


To ensure that a validation result is correctly associated with a field when using a custom
validation attribute, pass the validation context's MemberName when creating the
ValidationResult.

CustomValidator.cs :

C#

using System;
using System.ComponentModel.DataAnnotations;

public class CustomValidator : ValidationAttribute


{
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
...

return new ValidationResult("Validation message to user.",


new[] { validationContext.MemberName });
}
}

Inject services into custom validation attributes through the ValidationContext. The
following example demonstrates a salad chef form that validates user input with
dependency injection (DI).

The SaladChef class indicates the approved fruit ingredient list for a salad.

SaladChef.cs :

C#

public class SaladChef


{
public string[] ThingsYouCanPutInASalad = { "Strawberries", "Pineapple",
"Honeydew", "Watermelon", "Grapes" };
}

Register SaladChef in the app's DI container in Program.cs :

C#

builder.Services.AddTransient<SaladChef>();

The IsValid method of the following SaladChefValidatorAttribute class obtains the


SaladChef service from DI to check the user's input.
SaladChefValidatorAttribute.cs :

C#

using System.ComponentModel.DataAnnotations;

public class SaladChefValidatorAttribute : ValidationAttribute


{
protected override ValidationResult? IsValid(object? value,
ValidationContext validationContext)
{
var saladChef = validationContext.GetRequiredService<SaladChef>();

if (saladChef.ThingsYouCanPutInASalad.Contains(value?.ToString()))
{
return ValidationResult.Success;
}

return new ValidationResult("You should not put that in a salad!");


}
}

The following ValidationWithDI component validates user input by applying the


SaladChefValidatorAttribute ( [SaladChefValidator] ) to the salad ingredient string

( SaladIngredient ).

Pages/ValidationWithDI.razor :

razor

@page "/validation-with-di"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms

<EditForm Model="@this" autocomplete="off">


<DataAnnotationsValidator />

<p>
Name something you can put in a salad:
<input @bind="SaladIngredient" />
</p>

<button type="submit">Submit</button>

<ul>
@foreach (var message in context.GetValidationMessages())
{
<li class="validation-message">@message</li>
}
</ul>
</EditForm>

@code {
[SaladChefValidator]
public string? SaladIngredient { get; set; }
}

Custom validation CSS class attributes


Custom validation CSS class attributes are useful when integrating with CSS frameworks,
such as Bootstrap .

The following example uses the ExampleModel class.

ExampleModel.cs :

C#

using System.ComponentModel.DataAnnotations;

public class ExampleModel


{
[Required]
[StringLength(10, ErrorMessage = "Name is too long.")]
public string? Name { get; set; }
}

To specify custom validation CSS class attributes, start by providing CSS styles for
custom validation. In the following example, valid ( validField ) and invalid
( invalidField ) styles are specified.

wwwroot/css/app.css (Blazor WebAssembly) or wwwroot/css/site.css (Blazor Server):

css

.validField {
border-color: lawngreen;
}

.invalidField {
background-color: tomato;
}

Create a class derived from FieldCssClassProvider that checks for field validation
messages and applies the appropriate valid or invalid style.
CustomFieldClassProvider.cs :

C#

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider : FieldCssClassProvider


{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
var isValid =
!editContext.GetValidationMessages(fieldIdentifier).Any();

return isValid ? "validField" : "invalidField";


}
}

Set the CustomFieldClassProvider class as the Field CSS Class Provider on the form's
EditContext instance with SetFieldCssClassProvider.

Pages/FormExample8.razor :

razor

@page "/form-example-8"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample8> Logger

<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">


<DataAnnotationsValidator />
<ValidationSummary />

<InputText @bind-Value="exampleModel.Name" />

<button type="submit">Submit</button>
</EditForm>

@code {
private ExampleModel exampleModel = new();
private EditContext? editContext;

protected override void OnInitialized()


{
editContext = new(exampleModel);
editContext.SetFieldCssClassProvider(new
CustomFieldClassProvider());
}

private void HandleValidSubmit()


{
Logger.LogInformation("HandleValidSubmit called");
// Process the valid form
}
}

The preceding example checks the validity of all form fields and applies a style to each
field. If the form should only apply custom styles to a subset of the fields, make
CustomFieldClassProvider apply styles conditionally. The following

CustomFieldClassProvider2 example only applies a style to the Name field. For any fields
with names not matching Name , string.Empty is returned, and no style is applied. Using
reflection, the field is matched to the model member's property or field name, not an id
assigned to the HTML entity.

CustomFieldClassProvider2.cs :

C#

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider


{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
if (fieldIdentifier.FieldName == "Name")
{
var isValid =
!editContext.GetValidationMessages(fieldIdentifier).Any();

return isValid ? "validField" : "invalidField";


}

return string.Empty;
}
}

7 Note

Matching the field name in the preceding example is case sensitive, so a model
property member designated " Name " must match a conditional check on " Name ":

✔️ fieldIdentifier.FieldName == "Name"

❌ fieldIdentifier.FieldName == "name"

❌ fieldIdentifier.FieldName == "NAME"

❌ fieldIdentifier.FieldName == "nAmE"
Add an additional property to ExampleModel , for example:

C#

[StringLength(10, ErrorMessage = "Description is too long.")]


public string? Description { get; set; }

Add the Description to the ExampleForm7 component's form:

razor

<InputText @bind-Value="exampleModel.Description" />

Update the EditContext instance in the component's OnInitialized method to use the
new Field CSS Class Provider:

C#

editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());

Because a CSS validation class isn't applied to the Description field, it isn't styled.
However, field validation runs normally. If more than 10 characters are provided, the
validation summary indicates the error:

Description is too long.

In the following example:

The custom CSS style is applied to the Name field.

Any other fields apply logic similar to Blazor's default logic and using Blazor's
default field CSS validation styles, modified with valid or invalid . Note that for
the default styles, you don't need to add them to the app's stylesheet if the app is
based on a Blazor project template. For apps not based on a Blazor project
template, the default styles can be added to the app's stylesheet:

css

.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}

.invalid {
outline: 1px solid red;
}

CustomFieldClassProvider3.cs :

C#

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider3 : FieldCssClassProvider


{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
var isValid =
!editContext.GetValidationMessages(fieldIdentifier).Any();

if (fieldIdentifier.FieldName == "Name")
{
return isValid ? "validField" : "invalidField";
}
else
{
if (editContext.IsModified(fieldIdentifier))
{
return isValid ? "modified valid" : "modified invalid";
}
else
{
return isValid ? "valid" : "invalid";
}
}
}
}

Update the EditContext instance in the component's OnInitialized method to use the
preceding Field CSS Class Provider:

C#

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

Using CustomFieldClassProvider3 :

The Name field uses the app's custom validation CSS styles.
The Description field uses logic similar to Blazor's logic and Blazor's default field
CSS validation styles.
Blazor data annotations validation package
The Microsoft.AspNetCore.Components.DataAnnotations.Validation is a package that
fills validation experience gaps using the DataAnnotationsValidator component. The
package is currently experimental.

2 Warning

The Microsoft.AspNetCore.Components.DataAnnotations.Validation package


has a latest version of release candidate at NuGet.org . Continue to use the
experimental release candidate package at this time. Experimental features are
provided for the purpose of exploring feature viability and may not ship in a stable
version. Watch the Announcements GitHub repository , the dotnet/aspnetcore
GitHub repository , or this topic section for further updates.

Nested models, collection types, and complex


types
Blazor provides support for validating form input using data annotations with the built-
in DataAnnotationsValidator. However, the DataAnnotationsValidator only validates top-
level properties of the model bound to the form that aren't collection- or complex-type
properties.

To validate the bound model's entire object graph, including collection- and complex-
type properties, use the ObjectGraphDataAnnotationsValidator provided by the
experimental Microsoft.AspNetCore.Components.DataAnnotations.Validation package:

razor

<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">


<ObjectGraphDataAnnotationsValidator />
...
</EditForm>

Annotate model properties with [ValidateComplexType] . In the following model classes,


the ShipDescription class contains additional data annotations to validate when the
model is bound to the form:

Starship.cs :

C#
using System;
using System.ComponentModel.DataAnnotations;

public class Starship


{
...

[ValidateComplexType]
public ShipDescription ShipDescription { get; set; } = new();

...
}

ShipDescription.cs :

C#

using System;
using System.ComponentModel.DataAnnotations;

public class ShipDescription


{
[Required]
[StringLength(40, ErrorMessage = "Description too long (40 char).")]
public string? ShortDescription { get; set; }

[Required]
[StringLength(240, ErrorMessage = "Description too long (240 char).")]
public string? LongDescription { get; set; }
}

Enable the submit button based on form


validation
To enable and disable the submit button based on form validation, the following
example:

Uses a shortened version of the preceding Starfleet Starship Database form


( FormExample2 component) that only accepts a value for the ship's identifier. The
other Starship properties receive valid default values when an instance of the
Starship type is created.

Uses the form's EditContext to assign the model when the component is initialized.
Validates the form in the context's OnFieldChanged callback to enable and disable
the submit button.
Implements IDisposable and unsubscribes the event handler in the Dispose
method. For more information, see ASP.NET Core Razor component lifecycle.

7 Note

When assigning to the EditForm.EditContext, don't also assign an EditForm.Model


to the EditForm.

Pages/FormExample9.razor :

razor

@page "/form-example-9"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample9> Logger

<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">


<DataAnnotationsValidator />
<ValidationSummary />

<p>
<label>
Identifier:
<InputText @bind-Value="starship.Identifier" />
</label>
</p>

<button type="submit" disabled="@formInvalid">Submit</button>


</EditForm>

@code {
private Starship starship =
new()
{
Identifier = "NCC-1701",
Classification = "Exploration",
MaximumAccommodation = 150,
IsValidatedDesign = true,
ProductionDate = new DateTime(2245, 4, 11)
};
private bool formInvalid = false;
private EditContext? editContext;

protected override void OnInitialized()


{
editContext = new(starship);
editContext.OnFieldChanged += HandleFieldChanged;
}

private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)


{
if (editContext is not null)
{
formInvalid = !editContext.Validate();
StateHasChanged();
}
}

private void HandleValidSubmit()


{
Logger.LogInformation("HandleValidSubmit called");

// Process the valid form


}

public void Dispose()


{
if (editContext is not null)
{
editContext.OnFieldChanged -= HandleFieldChanged;
}
}
}

If a form isn't preloaded with valid values and you wish to disable the Submit button on
form load, set formInvalid to true .

A side effect of the preceding approach is that a validation summary


(ValidationSummary component) is populated with invalid fields after the user interacts
with any one field. Address this scenario in either of the following ways:

Don't use a ValidationSummary component on the form.


Make the ValidationSummary component visible when the submit button is
selected (for example, in a HandleValidSubmit method).

razor

<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">


<DataAnnotationsValidator />
<ValidationSummary style="@displaySummary" />

...

<button type="submit" disabled="@formInvalid">Submit</button>


</EditForm>

@code {
private string displaySummary = "display:none";

...
private void HandleValidSubmit()
{
displaySummary = "display:block";
}
}

Large form payloads and the SignalR message


size limit
This section only applies to Blazor Server apps and hosted Blazor WebAssembly solutions
that implement SignalR.

If form processing fails because the component's form payload has exceeded the
maximum incoming SignalR message size permitted for hub methods, the form can
adopt streaming JS interop without increasing the message size limit. For more
information on the size limit and the error thrown, see ASP.NET Core Blazor SignalR
guidance.

In the following example a text area ( <textarea> ) is used with streaming JS interop to
move up to 50,000 bytes of data to the server.

Add a JavaScript (JS) getText function to the app:

JavaScript

window.getText = (elem) => {


const textValue = elem.value;
const utf8Encoder = new TextEncoder();
const encodedTextValue = utf8Encoder.encode(textValue);
return encodedTextValue;
};

For information on where to place JS in a Blazor app, see ASP.NET Core Blazor JavaScript
interoperability (JS interop).

ExampleModel2.cs :

C#

public class ExampleModel2


{
public string TextAreaValue { get; set; } = string.Empty;
}
Due to security considerations, zero-length streams aren't permitted for streaming JS
Interop. Therefore, the following FormExample10 component traps a JSException and
returns an empty string if the text area is blank when the form is submitted.

Pages/FormExample10.razor :

razor

@page "/form-example-10"
@inject IJSRuntime JS
@inject ILogger<FormExample10> Logger

<h1>Stream form data with JS interop</h1>

<EditForm Model="@exampleModel" OnSubmit="@HandleSubmit">


<p>
<label>
&lt;textarea&gt; value streamed for assignment to
<code>TextAreaValue (&lt;= 50,000 characters)</code>:
<textarea @ref="largeTextArea" />
</label>
</p>

<button type="submit">Submit</button>
</EditForm>

<p>
TextAreaValue length: @exampleModel.TextAreaValue1.Length
</p>

@code {
private ExampleModel2 exampleModel = new();
private ElementReference largeTextArea;

private async Task HandleSubmit()


{
exampleModel.TextAreaValue = await GetTextAsync();

Logger.LogInformation("TextAreaValue length: {Length}",


exampleModel.TextAreaValue.Length);
}

public async Task<string> GetTextAsync()


{
try
{
var streamRef =
await JS.InvokeAsync<IJSStreamReference>("getText",
largeTextArea);
var stream = await streamRef.OpenReadStreamAsync(maxAllowedSize:
50_000);
var streamReader = new StreamReader(stream);
return await streamReader.ReadToEndAsync();
}
catch (JSException jsException)
{
if (jsException.InnerException is
ArgumentOutOfRangeException outOfRangeException &&
outOfRangeException.ActualValue is not null &&
outOfRangeException.ActualValue is long actualLength &&
actualLength == 0)
{
return string.Empty;
}

throw;
}
}
}

Troubleshoot

EditForm parameter error


InvalidOperationException: EditForm requires a Model parameter, or an EditContext
parameter, but not both.

Confirm that the EditForm assigns a Model or an EditContext. Don't use both for the
same form.

When assigning to Model, confirm that the model type is instantiated, as the following
example shows:

C#

private ExampleModel exampleModel = new();

Connection disconnected
Error: Connection disconnected with error 'Error: Server returned an error on close:
Connection closed with an error.'.

System.IO.InvalidDataException: The maximum message size of 32768B was


exceeded. The message size can be configured in AddHubOptions.
For more information and guidance, see the following resources:

Large form payloads and the SignalR message size limit


ASP.NET Core Blazor SignalR guidance

Additional resources
ASP.NET Core Blazor file uploads
Secure a hosted ASP.NET Core Blazor WebAssembly app with Azure Active
Directory
Secure a hosted ASP.NET Core Blazor WebAssembly app with Azure Active
Directory B2C
Secure a hosted ASP.NET Core Blazor WebAssembly app with Identity Server
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor file uploads
Article • 03/08/2023

This article explains how to upload files in Blazor with the InputFile component.

2 Warning

Always follow security best practices when permitting users to upload files. For
more information, see Upload files in ASP.NET Core.

Use the InputFile component to read browser file data into .NET code. The InputFile
component renders an HTML <input> element of type file . By default, the user selects
single files. Add the multiple attribute to permit the user to upload multiple files at
once.

File selection isn't cumulative when using an InputFile component or its underlying
HTML <input type="file"> , so you can't add files to an existing file selection. The
component always replaces the user's initial file selection, so file references from prior
selections aren't available.

The following InputFile component executes the LoadFiles method when the
OnChange (change ) event occurs. An InputFileChangeEventArgs provides access to
the selected file list and details about each file:

razor

<InputFile OnChange="@LoadFiles" multiple />

@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}

Rendered HTML:

HTML

<input multiple="" type="file" _bl_2="">

7 Note
In the preceding example, the <input> element's _bl_2 attribute is used for
Blazor's internal processing.

To read data from a user-selected file, call IBrowserFile.OpenReadStream on the file and
read from the returned stream. For more information, see the File streams section.

OpenReadStream enforces a maximum size in bytes of its Stream. Reading one file or
multiple files larger than 500 KB results in an exception. This limit prevents developers
from accidentally reading large files into memory. The maxAllowedSize parameter of
OpenReadStream can be used to specify a larger size if required.

If you need access to a Stream that represents the file's bytes, use
IBrowserFile.OpenReadStream. Avoid reading the incoming file stream directly into
memory all at once. For example, don't copy all of the file's bytes into a MemoryStream
or read the entire stream into a byte array all at once. These approaches can result in
performance and security problems, especially for Blazor Server apps. Instead, consider
adopting either of the following approaches:

On the server of a hosted Blazor WebAssembly app or a Blazor Server app, copy
the stream directly to a file on disk without reading it into memory. Note that
Blazor apps aren't able to access the client's file system directly.
Upload files from the client directly to an external service. For more information,
see the Upload files to an external service section.

In the following examples, browserFile represents the uploaded file and implements
IBrowserFile. Working implementations for IBrowserFile are shown in the file upload
components later in this article.

❌ The following approach is NOT recommended because the file's Stream content is
read into a String in memory ( reader ):

C#

var reader =
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();

❌ The following approach is NOT recommended for Microsoft Azure Blob Storage
because the file's Stream content is copied into a MemoryStream in memory
( memoryStream ) before calling UploadBlobAsync:

C#
var memoryStream = new MemoryStream();
browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFileName, memoryStream));

✔️The following approach is recommended because the file's Stream is provided


directly to the consumer, a FileStream that creates the file at the provided path:

C#

await using FileStream fs = new(path, FileMode.Create);


await browserFile.OpenReadStream().CopyToAsync(fs);

✔️The following approach is recommended for Microsoft Azure Blob Storage because
the file's Stream is provided directly to UploadBlobAsync:

C#

await blobContainerClient.UploadBlobAsync(
trustedFileName, browserFile.OpenReadStream());

A component that receives an image file can call the


BrowserFileExtensions.RequestImageFileAsync convenience method on the file to resize
the image data within the browser's JavaScript runtime before the image is streamed
into the app. Use cases for calling RequestImageFileAsync are most appropriate for
Blazor WebAssembly apps.

File size read and upload limits


There's no file size read or upload limit for the InputFile component in Blazor Server
apps.

Upload files example


The following example demonstrates multiple file upload in a component.
InputFileChangeEventArgs.GetMultipleFiles allows reading multiple files. Specify the
maximum number of files to prevent a malicious user from uploading a larger number
of files than the app expects. InputFileChangeEventArgs.File allows reading the first and
only file if the file upload doesn't support multiple files.

7 Note
InputFileChangeEventArgs is in the Microsoft.AspNetCore.Components.Forms
namespace, which is typically one of the namespaces in the app's _Imports.razor
file. When the namespace is present in the _Imports.razor file, it provides API
member access to the app's components:

razor

using Microsoft.AspNetCore.Components.Forms

Namespaces in the _Imports.razor file aren't applied to C# files ( .cs ). C# files


require an explicit using directive.

7 Note

For testing file upload components, you can create test files of any size with
PowerShell:

PowerShell

$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out);


[IO.File]::WriteAllBytes('{PATH}', $out)

In the preceding command:

The {SIZE} placeholder is the size of the file in bytes (for example, 2097152
for a 2 MB file).
The {PATH} placeholder is the path and file with file extension (for example,
D:/test_files/testfile2MB.txt ).

Pages/FileUpload1.razor :

razor

@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment

<h3>Upload Files</h3>

<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>

<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>

<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="@LoadFiles" multiple />
</label>
</p>

@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}

@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;

private async Task LoadFiles(InputFileChangeEventArgs e)


{
isLoading = true;
loadedFiles.Clear();

foreach (var file in e.GetMultipleFiles(maxAllowedFiles))


{
try
{
loadedFiles.Add(file);

var trustedFileNameForFileStorage =
Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);

await using FileStream fs = new(path, FileMode.Create);


await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}

isLoading = false;
}
}

IBrowserFile returns metadata exposed by the browser as properties. Use this


metadata for preliminary validation.

2 Warning

Never trust the values of the following properties, especially the Name property for
display in the UI. Treat all user-supplied data as a significant security risk to the app,
server, and network. For more information, see Upload files in ASP.NET Core.

Name
Size
LastModified
ContentType

Upload files to a server


The following example demonstrates uploading files from a Blazor Server app to a
backend web API controller in a separate app, possibly on a separate server.

In the Blazor Server app, add IHttpClientFactory and related services that allow the app
to create HttpClient instances.

In Program.cs :
C#

builder.Services.AddHttpClient();

For more information, see Make HTTP requests using IHttpClientFactory in ASP.NET
Core.

For the examples in this section:

The web API runs at the URL: https://localhost:5001


The Blazor Server app runs at the URL: https://localhost:5003

For testing, the preceding URLs are configured in the projects'


Properties/launchSettings.json files.

Upload result class


The following UploadResult class is placed in the client project and in the web API
project to maintain the result of an uploaded file. When a file fails to upload on the
server, an error code is returned in ErrorCode for display to the user. A safe file name is
generated on the server for each file and returned to the client in StoredFileName for
display. Files are keyed between the client and server using the unsafe/untrusted file
name in FileName .

UploadResult.cs :

C#

public class UploadResult


{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}

7 Note

A security best practice for production apps is to avoid sending error messages to
clients that might reveal sensitive information about an app, server, or network.
Providing detailed error messages can aid a malicious user in devising attacks on
an app, server, or network. The example code in this section only sends back an
error code number ( int ) for display by the component client-side if a server-side
error occurs. If a user requires assistance with a file upload, they provide the error
code to support personnel for support ticket resolution without ever knowing the
exact cause of the error.

Upload component
The following FileUpload2 component:

Permits users to upload files from the client.


Displays the untrusted/unsafe file name provided by the client in the UI. The
untrusted/unsafe file name is automatically HTML-encoded by Razor for safe
display in the UI.

2 Warning

Don't trust file names supplied by clients for:

Saving the file to a file system or service.


Display in UIs that don't encode file names automatically or via developer
code.

For more information on security considerations when uploading files to a server,


see Upload files in ASP.NET Core.

Pages/FileUpload2.razor in the Blazor Server app:

razor

@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="@OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}

@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;

protected override bool ShouldRender() => shouldRender;

private async Task OnInputFileChange(InputFileChangeEventArgs e)


{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;

using var content = new MultipartFormDataContent();

foreach (var file in e.GetMultipleFiles(maxAllowedFiles))


{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent =
new StreamContent(file.OpenReadStream(maxFileSize));

fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);

content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);

upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);

uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}

if (upload)
{
var client = ClientFactory.CreateClient();

var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);

if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};

using var responseStream =


await response.Content.ReadAsStreamAsync();

var newUploadResults = await JsonSerializer


.DeserializeAsync<IList<UploadResult>>(responseStream,
options);

if (newUploadResults is not null)


{
uploadResults =
uploadResults.Concat(newUploadResults).ToList();
}
}
}

shouldRender = true;
}

private static bool FileUpload(IList<UploadResult> uploadResults,


string? fileName, ILogger<FileUpload2> logger, out UploadResult
result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName)
?? new();

if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)",
fileName);
result.ErrorCode = 5;
}

return result.Uploaded;
}

private class File


{
public string? Name { get; set; }
}
}

Upload controller
The following controller in the web API project saves uploaded files from the client.

) Important

The controller in this section is intended for use in a separate web API project from
the Blazor Server app.

To use the following code, create a Development/unsafe_uploads folder at the root of


the web API project for the app running in the Development environment.

Because the example uses the app's environment as part of the path where files are
saved, additional folders are required if other environments are used in testing and
production. For example, create a Staging/unsafe_uploads folder for the Staging
environment. Create a Production/unsafe_uploads folder for the Production
environment.

2 Warning

The example saves files without scanning their contents, and the guidance in this
article doesn't take into account additional security best practices for uploaded
files. On staging and production systems, disable execute permission on the upload
folder and scan files with an anti-virus/anti-malware scanner API immediately after
upload. For more information, see Upload files in ASP.NET Core.

Controllers/FilesaveController.cs :

C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
private readonly IWebHostEnvironment env;
private readonly ILogger<FilesaveController> logger;

public FilesaveController(IWebHostEnvironment env,


ILogger<FilesaveController> logger)
{
this.env = env;
this.logger = logger;
}

[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = new();

foreach (var file in files)


{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);

if (filesProcessed < maxAllowedFiles)


{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is "
+
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length,
maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage =
Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);

await using FileStream fs = new(path,


FileMode.Create);
await file.CopyToAsync(fs);

logger.LogInformation("{FileName} saved at {Path}",


trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName =
trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err:
3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}

filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the "
+
"request exceeded the allowed {Count} of files (Err:
4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}

uploadResults.Add(uploadResult);
}

return new CreatedResult(resourcePath, uploadResults);


}
}

In the preceding code, GetRandomFileName is called to generate a secure file name.


Never trust the file name provided by the browser, as an attacker may choose an
existing file name that overwrites an existing file or send a path that attempts to write
outside of the app.

Cancel a file upload


A file upload component can detect when a user has cancelled an upload by using a
CancellationToken when calling into the IBrowserFile.OpenReadStream or
StreamReader.ReadAsync.

Create a CancellationTokenSource for the InputFile component. At the start of the


OnInputFileChange method, check if a previous upload is in progress.

If a file upload is in progress:

Call Cancel on the previous upload.


Create a new CancellationTokenSource for the next upload and pass the
CancellationTokenSource.Token to OpenReadStream or ReadAsync.

Upload files with progress


The following example demonstrates how to upload files in a Blazor Server app with
upload progress displayed to the user.

To use the following example in a test app:


Create a folder to save uploaded files for the Development environment:
Development/unsafe_uploads .
Configure the maximum file size ( maxFileSize , 15 KB in the following example) and
maximum number of allowed files ( maxAllowedFiles , 3 in the following example).
Set the buffer to a different value (10 KB in the following example), if desired, for
increased granularity in progress reporting. We don't recommended using a buffer
larger than 30 KB due to performance and security concerns.

2 Warning

The example saves files without scanning their contents, and the guidance in this
article doesn't take into account additional security best practices for uploaded
files. On staging and production systems, disable execute permission on the upload
folder and scan files with an anti-virus/anti-malware scanner API immediately after
upload. For more information, see Upload files in ASP.NET Core.

Pages/FileUpload3.razor :

razor

@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment

<h3>Upload Files</h3>

<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>

<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>

<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="@LoadFiles" multiple />
</label>
</p>

@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}

@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;

private async Task LoadFiles(InputFileChangeEventArgs e)


{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;

foreach (var file in e.GetMultipleFiles(maxAllowedFiles))


{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileName);

await using FileStream writeStream = new(path,


FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];

while ((bytesRead = await readStream.ReadAsync(buffer)) !=


0)
{
totalRead += bytesRead;

await writeStream.WriteAsync(buffer, 0, bytesRead);

progressPercent = Decimal.Divide(totalRead, file.Size);

StateHasChanged();
}

loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}

isLoading = false;
}
}

For more information, see the following API resources:

FileStream: Provides a Stream for a file, supporting both synchronous and


asynchronous read and write operations.
FileStream.ReadAsync: The preceding FileUpload3 component reads the stream
asynchronously with ReadAsync. Reading a stream synchronously with Read isn't
supported in Razor components.

File streams
In Blazor Server, file data is streamed over the SignalR connection into .NET code on the
server as the file is read.

Upload image preview


For an image preview of uploading images, start by adding an InputFile component
with a component reference and an OnChange handler:

razor

<InputFile @ref="inputFile" OnChange="@ShowPreview" />


Add an image element with an element reference, which serves as the placeholder for
the image preview:

razor

<img @ref="previewImageElem" />

Add the associated references:

razor

@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
}

In JavaScript, add a function called with an HTML input and img element that
performs the following:

Extracts the selected file.


Creates an object URL with createObjectURL .
Sets an event listener to revoke the object URL with revokeObjectURL after the
image is loaded, so memory isn't leaked.
Sets the img element's source to display the image.

JavaScript

window.previewImage = (inputElem, imgElem) => {


const url = URL.createObjectURL(inputElem.files[0]);
imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once:
true });
imgElem.src = url;
}

Finally, use an injected IJSRuntime to add the OnChange handler that calls the JavaScript
function:

razor

@inject IJSRuntime JS

...

@code {
...
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}

The preceding example is for uploading a single image. The approach can be expanded
to support multiple images.

The following FileUpload4 component shows the complete example.

Pages/FileUpload4.razor :

razor

@page "/file-upload-4"
@inject IJSRuntime JS

<h1>File Upload Example</h1>

<InputFile @ref="inputFile" OnChange="@ShowPreview" />

<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />

@code {
private InputFile? inputFile;
private ElementReference previewImageElem;

private async Task ShowPreview() => await JS.InvokeVoidAsync(


"previewImage", inputFile!.Element, previewImageElem);
}

Upload files to an external service


Instead of an app handling file upload bytes and the app's server receiving uploaded
files, clients can directly upload files to an external service. The app can safely process
the files from the external service on demand. This approach hardens the app and its
server against malicious attacks and potential performance problems.

Consider an approach that uses Azure Files , Azure Blob Storage , or a third-party
service with the following potential benefits:

Upload files from the client directly to an external service with a JavaScript client
library or REST API. For example, Azure offers the following client libraries and APIs:
Azure Storage File Share client library
Azure Files REST API
Azure Storage Blob client library for JavaScript
Blob service REST API
Authorize user uploads with a user-delegated shared-access signature (SAS) token
generated by the app (server-side) for each client file upload. For example, Azure
offers the following SAS features:
Azure Storage File Share client library for JavaScript: with SAS Token
Azure Storage Blob client library for JavaScript: with SAS Token
Provide automatic redundancy and file share backup.
Limit uploads with quotas. Note that Azure Blob Storage's quotas are set at the
account level, not the container level. However, Azure Files quotas are at the file
share level and might provide better control over upload limits. For more
information, see the Azure documents linked earlier in this list.
Secure files with server-side encryption (SSE).

For more information on Azure Blob Storage and Azure Files, see the Azure Storage
documentation.

SignalR message size limit


File uploads may fail even before they start, when Blazor retrieves data about the files
that exceeds the maximum SignalR message size.

SignalR defines a message size limit that applies to every message Blazor receives, and
the InputFile component streams files to the server in messages that respect the
configured limit. However, the first message, which indicates the set of files to upload, is
sent as a unique single message. The size of the first message may exceed the SignalR
message size limit. The issue isn't related to the size of the files, it's related to the
number of files.

The logged error is similar to the following:

Error: Connection disconnected with error 'Error: Server returned an error on close:
Connection closed with an error.'. e.log @ blazor.server.js:1

When uploading files, reaching the message size limit on the first message is rare. If the
limit is reached, the app can configure HubOptions.MaximumReceiveMessageSize with a
larger value.

For more information on SignalR configuration and how to set


MaximumReceiveMessageSize, see ASP.NET Core Blazor SignalR guidance.

Additional resources
ASP.NET Core Blazor file downloads
Upload files in ASP.NET Core
ASP.NET Core Blazor forms and input components
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor file downloads
Article • 04/04/2023

This article explains how to download files in Blazor Server and Blazor WebAssembly
apps.

Files can be downloaded from the app's own static assets or from any other location:

ASP.NET Core apps use Static File Middleware to serve files to clients of Blazor
Server and hosted Blazor WebAssembly apps.
The guidance in this article also applies to other types of file servers that don't use
.NET, such as Content Delivery Networks (CDNs).

This article covers approaches for the following scenarios:

Stream file content to a raw binary data buffer on the client: Typically, this
approach is used for relatively small files (< 250 MB).
Download a file via a URL without streaming: Usually, this approach is used for
relatively large files (> 250 MB).

When downloading files from a different origin than the app, Cross-Origin Resource
Sharing (CORS) considerations apply. For more information, see the Cross-Origin
Resource Sharing (CORS) section.

Security considerations
Use caution when providing users with the ability to download files from a server.
Attackers may execute denial of service (DOS) attacks, API exploitation attacks , or
attempt to compromise networks and servers in other ways.

Security steps that reduce the likelihood of a successful attack are:

Download files from a dedicated file download area on the server, preferably from
a non-system drive. Using a dedicated location makes it easier to impose security
restrictions on downloadable files. Disable execute permissions on the file
download area.
Client-side security checks are easy to circumvent by malicious users. Always
perform client-side security checks on the server, too.
Don't receive files from users or other untrusted sources and then make the files
available for immediate download without performing security checks on the files.
For more information, see Upload files in ASP.NET Core.
Download from a stream
This section applies to files that are typically up to 250 MB in size.

The recommended approach for downloading relatively small files (< 250 MB) is to
stream file content to a raw binary data buffer on the client with JavaScript (JS) interop.

2 Warning

The approach in this section reads the file's content into a JS ArrayBuffer . This
approach loads the entire file into the client's memory, which can impair
performance. To download relatively large files (>= 250 MB), we recommend
following the guidance in the Download from a URL section.

The following downloadFileFromStream JS function performs the following steps:

Read the provided stream into an ArrayBuffer .


Create a Blob to wrap the ArrayBuffer .
Create an object URL to serve as the file's download address.
Create an HTMLAnchorElement ( <a> element).
Assign the file's name ( fileName ) and URL ( url ) for the download.
Trigger the download by firing a click event on the anchor element.
Remove the anchor element.
Revoke the object URL ( url ) by calling URL.revokeObjectURL . This is an
important step to ensure memory isn't leaked on the client.

HTML

<script>
window.downloadFileFromStream = async (fileName, contentStreamReference)
=> {
const arrayBuffer = await contentStreamReference.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
URL.revokeObjectURL(url);
}
</script>

7 Note
For general guidance on JS location and our recommendations for production
apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

The following example component:

Uses native byte-streaming interop to ensure efficient transfer of the file to the
client.
Has a method named GetFileStream to retrieve a Stream for the file that's
downloaded to clients. Alternative approaches include retrieving a file from
storage or generating a file dynamically in C# code. For this demonstration, the
app creates a 50 KB file of random data from a new byte array ( new byte[] ). The
bytes are wrapped with a MemoryStream to serve as the example's dynamically-
generated binary file.
The DownloadFileFromStream method performs the following steps:
Retrieve the Stream from GetFileStream .
Specify a file name when file is saved on the user's machine. The following
example names the file quote.txt .
Wrap the Stream in a DotNetStreamReference, which allows streaming the file
data to the client.
Invoke the downloadFileFromStream JS function to accept the data on the client.

razor

@page "/file-download-1"
@using System.IO
@inject IJSRuntime JS

<h1>File Download Example</h1>

<button @onclick="DownloadFileFromStream">
Download File From Stream
</button>

@code {
private Stream GetFileStream()
{
var randomBinaryData = new byte[50 * 1024];
var fileStream = new MemoryStream(randomBinaryData);

return fileStream;
}

private async Task DownloadFileFromStream()


{
var fileStream = GetFileStream();
var fileName = "log.bin";
using var streamRef = new DotNetStreamReference(stream: fileStream);

await JS.InvokeVoidAsync("downloadFileFromStream", fileName,


streamRef);
}
}

For a component in a Blazor Server app that must return a Stream for a physical file, the
component can call File.OpenRead, as the following example demonstrates:

C#

private Stream GetFileStream()


{
return File.OpenRead(@"{PATH}");
}

In the preceding example, the {PATH} placeholder is the path to the file. The @ prefix
indicates that the string is a verbatim string literal, which permits the use of backslashes
( \ ) in a Windows OS path and embedded double-quotes ( "" ) for a single quote in the
path. Alternatively, avoid the string literal ( @ ) and use either of the following
approaches:

Use escaped backslashes ( \\ ) and quotes ( \" ).


Use forward slashes ( / ) in the path, which are supported across platforms in
ASP.NET Core apps, and escaped quotes ( \" ).

Download from a URL


This section applies to files that are relatively large, typically 250 MB or larger.

The example in this section uses a download file named quote.txt , which is placed in a
folder named files in the app's web root ( wwwroot folder). The use of the files folder
is only for demonstration purposes. You can organize downloadable files in any folder
layout within the web root ( wwwroot folder) that you prefer, including serving the files
directly from the wwwroot folder.

wwwroot/files/quote.txt :

text

When victory is ours, we'll wipe every trace of the Thals and their city
from the face of this land. We will avenge the deaths of all Kaleds who've
fallen in the cause of right and justice and build a peace which will be a
monument to their sacrifice. Our battle cry will be "Total extermination of
the Thals!"

- General Ravon (Guy Siner, http://guysiner.com/)


Dr. Who: Genesis of the Daleks (https://www.bbc.co.uk/programmes/p00pq2gc)
©1975 BBC (https://www.bbc.co.uk/)

The following triggerFileDownload JS function performs the following steps:

Create an HTMLAnchorElement ( <a> element).


Assign the file's name ( fileName ) and URL ( url ) for the download.
Trigger the download by firing a click event on the anchor element.
Remove the anchor element.

HTML

<script>
window.triggerFileDownload = (fileName, url) => {
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
}
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

The following example component downloads the file from the same origin that the app
uses. If the file download is attempted from a different origin, configure Cross-Origin
Resource Sharing (CORS). For more information, see the Cross-Origin Resource Sharing
(CORS) section.

razor

@page "/file-download-2"
@inject IJSRuntime JS

<h1>File Download Example 2</h1>

<button @onclick="DownloadFileFromURL">
Download File From URL
</button>
@code {
private async Task DownloadFileFromURL()
{
var fileName = "quote.txt";
var fileURL = "https://localhost:5001/files/quote.txt";
await JS.InvokeVoidAsync("triggerFileDownload", fileName, fileURL);
}
}

Cross-Origin Resource Sharing (CORS)


Without taking further steps to enable Cross-Origin Resource Sharing (CORS) for files
that don't have the same origin as the app, downloading files won't pass CORS checks
made by the browser.

For more information on CORS with ASP.NET Core apps and other Microsoft products
and services that host files for download, see the following resources:

Enable Cross-Origin Requests (CORS) in ASP.NET Core


Using Azure CDN with CORS (Azure documentation)
Cross-Origin Resource Sharing (CORS) support for Azure Storage (REST
documentation)
Core Cloud Services - Set up CORS for your website and storage assets (Learn
module)
IIS CORS module Configuration Reference (IIS documentation)

Additional resources
ASP.NET Core Blazor JavaScript interoperability (JS interop)
<a>: The Anchor element: Security and privacy (MDN documentation)
ASP.NET Core Blazor file uploads
ASP.NET Core Blazor forms and input components
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor JavaScript
interoperability (JS interop)
Article • 04/27/2023

This article explains general concepts on how to interact with JavaScript in Blazor apps.

A Blazor app can invoke JavaScript (JS) functions from .NET methods and .NET methods
from JS functions. These scenarios are called JavaScript interoperability (JS interop).

Further JS interop guidance is provided in the following articles:

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


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

7 Note

JavaScript [JSImport] / [JSExport] interop API is available for Blazor WebAssembly


apps in ASP.NET Core 7.0 or later.

For more information, see JavaScript JSImport/JSExport interop with ASP.NET


Core Blazor WebAssembly.

JavaScript interop abstractions and features


package
The @microsoft/dotnet-js-interop package (npmjs.com) provides abstractions and
features for interop between .NET and JavaScript (JS) code. Reference source is available
in the dotnet/aspnetcore GitHub repository (/src/JSInterop folder) . For more
information, see the GitHub repository's README.md file.

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Additional resources for writing JS interop scripts in TypeScript:


TypeScript
Tutorial: Create an ASP.NET Core app with TypeScript in Visual Studio
Manage npm packages in Visual Studio

Interaction with the Document Object Model


(DOM)
Only mutate the Document Object Model (DOM) with JavaScript (JS) when the object
doesn't interact with Blazor. Blazor maintains representations of the DOM and interacts
directly with DOM objects. If an element rendered by Blazor is modified externally using
JS directly or via JS Interop, the DOM may no longer match Blazor's internal
representation, which can result in undefined behavior. Undefined behavior may merely
interfere with the presentation of elements or their functions but may also introduce
security risks to the app or server.

This guidance not only applies to your own JS interop code but also to any JS libraries
that the app uses, including anything provided by a third-party framework, such as
Bootstrap JS and jQuery .

In a few documentation examples, JS interop is used to mutate an element purely for


demonstration purposes as part of an example. In those cases, a warning appears in the
text.

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

Asynchronous JavaScript calls


JS interop calls are asynchronous by default, regardless of whether the called code is
synchronous or asynchronous. Calls are asynchronous by default to ensure that
components are compatible across both Blazor hosting models, Blazor Server and Blazor
WebAssembly. On Blazor Server, JS interop calls must be asynchronous because they're
sent over a network connection. For apps that exclusively adopt the Blazor
WebAssembly hosting model, synchronous JS interop calls are supported.

For more information, see the following articles:

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


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

Object serialization
Blazor uses System.Text.Json for serialization with the following requirements and
default behaviors:

Types must have a default constructor, get/set accessors must be public, and fields
are never serialized.
Global default serialization isn't customizable to avoid breaking existing
component libraries, impacts on performance and security, and reductions in
reliability.
Serializing .NET member names results in lowercase JSON key names.
JSON is deserialized as JsonElement C# instances, which permit mixed casing.
Internal casting for assignment to C# model properties works as expected in spite
of any case differences between JSON key names and C# property names.

JsonConverter API is available for custom serialization. Properties can be annotated with
a [JsonConverter] attribute to override default serialization for an existing data type.

For more information, see the following resources in the .NET documentation:

JSON serialization and deserialization (marshalling and unmarshalling) in .NET


How to customize property names and values with System.Text.Json
How to write custom converters for JSON serialization (marshalling) in .NET

Blazor supports optimized byte array JS interop that avoids encoding/decoding byte
arrays into Base64. The app can apply custom serialization and pass the resulting bytes.
For more information, see Call JavaScript functions from .NET methods in ASP.NET Core
Blazor.

JavaScript location
Load JavaScript (JS) code using any of the following approaches:

Load a script in <head> markup (Not generally recommended)


Load a script in <body> markup
Load a script from an external JavaScript file (.js) collocated with a component
Load a script from an external JavaScript file (.js)
Inject a script before or after Blazor starts

2 Warning

Don't place a <script> tag in a Razor component file ( .razor ) because the
<script> tag can't be updated dynamically by Blazor.
7 Note

Documentation examples usually place scripts in a <script> tag or load global


scripts from external files. These approaches pollute the client with global functions.
For production apps, we recommend placing JavaScript into separate JavaScript
modules that can be imported when needed. For more information, see the
JavaScript isolation in JavaScript modules section.

Load a script in <head> markup


The approach in this section isn't generally recommended.

Place the JavaScript (JS) tags ( <script>...</script> ) in the <head> element markup of
wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

HTML

<head>
...

<script>
window.jsMethod = (methodParameter) => {
...
};
</script>
</head>

Loading JS from the <head> isn't the best approach for the following reasons:

JS interop may fail if the script depends on Blazor. We recommend loading scripts
using one of the other approaches, not via the <head> markup.
The page may become interactive slower due to the time it takes to parse the JS in
the script.

Load a script in <body> markup


Place the JavaScript (JS) tags ( <script>...</script> ) inside the closing </body> element
markup of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor
Server):

HTML

<body>
...
<script src="_framework/blazor.{server|webassembly}.js"></script>
<script>
window.jsMethod = (methodParameter) => {
...
};
</script>
</body>

The {server|webassembly} placeholder in the preceding markup is either server for a


Blazor Server app ( blazor.server.js ) or webassembly for a Blazor WebAssembly app
( blazor.webassembly.js ).

Load a script from an external JavaScript file ( .js )


collocated with a component
Collocation of JavaScript (JS) files for pages, views, and Razor components is a
convenient way to organize scripts in an app.

Collocate JS files using the following filename extension conventions:

Pages of Razor Pages apps and views of MVC apps: .cshtml.js . Examples:
Pages/Index.cshtml.js for the Index page of a Razor Pages app at

Pages/Index.cshtml .
Views/Home/Index.cshtml.js for the Index view of an MVC app at

Views/Home/Index.cshtml .

Razor components of Blazor apps: .razor.js . Example: Pages/Index.razor.js for


the Index component at Pages/Index.razor .

Collocated JS files are publicly addressable using the path to the file in the project:

Pages, views, and components from a collocated scripts file in the app:

{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js


The {PATH} placeholder is the path to the page, view, or component.
The {PAGE, VIEW, OR COMPONENT} placeholder is the page, view, or component.
The {EXTENSION} placeholder matches the extension of the page, view, or
component, either razor or cshtml .

Razor Pages example:

A JS file for the Index page is placed in the Pages folder ( Pages/Index.cshtml.js )
next to the Index page ( Pages/Index.cshtml ). In the Index page, the script is
referenced at the path in the Pages folder:

razor

@section Scripts {
<script src="~/Pages/Index.cshtml.js"></script>
}

When the app is published, the framework automatically moves the script to the
web root. In the preceding example, the script is moved to bin\Release\{TARGET
FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.cshtml.js , where the {TARGET
FRAMEWORK MONIKER} placeholder is the Target Framework Moniker (TFM). No

change is required to the script's relative URL in the Index page.

Blazor example:

A JS file for the Index component is placed in the Pages folder


( Pages/Index.razor.js ) next to the Index component ( Pages/Index.razor ). In the
Index component, the script is referenced at the path in the Pages folder.

Pages/Index.razor.js :

JavaScript

export function showPrompt(message) {


return prompt(message, 'Type anything here');
}

In the OnAfterRenderAsync method of the Index component ( Pages/Index.razor ):

razor

module = await JS.InvokeAsync<IJSObjectReference>(


"import", "./Pages/Index.razor.js");

When the app is published, the framework automatically moves the script to the
web root. In the preceding example, the script is moved to bin\Release\{TARGET
FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.razor.js , where the {TARGET

FRAMEWORK MONIKER} placeholder is the Target Framework Moniker (TFM). No


change is required to the script's relative URL in the Index component.

For scripts provided by a Razor class library (RCL):

_content/{PACKAGE ID}/{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js


The {PACKAGE ID} placeholder is the RCL's package identifier (or library name
for a class library referenced by the app).
The {PATH} placeholder is the path to the page, view, or component. If a Razor
component is located at the root of the RCL, the path segment isn't included.
The {PAGE, VIEW, OR COMPONENT} placeholder is the page, view, or component.
The {EXTENSION} placeholder matches the extension of page, view, or
component, either razor or cshtml .

In the following Blazor app example:


The RCL's package identifier is AppJS .
A module's scripts are loaded for the Index component ( Index.razor ).
The Index component is in the Pages folder of the RCL.

C#

var module = await JS.InvokeAsync<IJSObjectReference>("import",


"./_content/AppJS/Pages/Index.razor.js");

For more information on RCLs, see Consume ASP.NET Core Razor components from a
Razor class library (RCL).

Load a script from an external JavaScript file ( .js )


Place the JavaScript (JS) tags ( <script>...</script> ) with a script source ( src ) path
inside the closing </body> tag after the Blazor script reference.

In wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

HTML

<body>
...

<script src="_framework/blazor.{server|webassembly}.js"></script>
<script src="{SCRIPT PATH AND FILE NAME (.js)}"></script>
</body>

The {server|webassembly} placeholder in the preceding markup is either server for a


Blazor Server app ( blazor.server.js ) or webassembly for a Blazor WebAssembly app
( blazor.webassembly.js ). The {SCRIPT PATH AND FILE NAME (.js)} placeholder is the
path and script file name under wwwroot .
In the following example of the preceding <script> tag, the scripts.js file is in the
wwwroot/js folder of the app:

HTML

<script src="js/scripts.js"></script>

When the external JS file is supplied by a Razor class library, specify the JS file using its
stable static web asset path: ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILE NAME
(.js)} :

The path segment for the current directory ( ./ ) is required in order to create the
correct static asset path to the JS file.
The {PACKAGE ID} placeholder is the library's package ID. The package ID defaults
to the project's assembly name if <PackageId> isn't specified in the project file.
The {SCRIPT PATH AND FILE NAME (.js)} placeholder is the path and file name
under wwwroot .

HTML

<body>
...

<script src="_framework/blazor.{server|webassembly}.js"></script>
<script src="./_content/{PACKAGE ID}/{SCRIPT PATH AND FILE NAME (.js)}">
</script>
</body>

In the following example of the preceding <script> tag:

The Razor class library has an assembly name of ComponentLibrary , and a


<PackageId> isn't specified in the library's project file.
The scripts.js file is in the class library's wwwroot folder.

HTML

<script src="./_content/ComponentLibrary/scripts.js"></script>

For more information, see Consume ASP.NET Core Razor components from a Razor class
library (RCL).

Inject a script before or after Blazor starts


To ensure scripts load before or after Blazor starts, use a JavaScript initializer. For more
information and examples, see ASP.NET Core Blazor startup.

JavaScript isolation in JavaScript modules


Blazor enables JavaScript (JS) isolation in standard JavaScript modules (ECMAScript
specification ).

JS isolation provides the following benefits:

Imported JS no longer pollutes the global namespace.


Consumers of a library and components aren't required to import the related JS.

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

Dynamic import with the import() operator is supported with ASP.NET Core and
Blazor:

JavaScript

if ({CONDITION}) import("/additionalModule.js");

In the preceding example, the {CONDITION} placeholder represents a conditional check


to determine if the module should be loaded.

For browser compatibility, see Can I use: JavaScript modules: dynamic import .

Cached JavaScript files


JavaScript (JS) files and other static assets aren't generally cached on clients during
development in the Development environment. During development, static asset
requests include the Cache-Control header with a value of no-cache or max-age
with a value of zero ( 0 ).

During production in the Production environment, JS files are usually cached by clients.

To disable client-side caching in browsers, developers usually adopt one of the following
approaches:

Disable caching when the browser's developer tools console is open. Guidance can
be found in the developer tools documentation of each browser maintainer:
Chrome DevTools
Firefox Developer Tools
Microsoft Edge Developer Tools overview
Perform a manual browser refresh of any webpage of the Blazor app to reload JS
files from the server. ASP.NET Core's HTTP Caching Middleware always honors a
valid no-cache Cache-Control header sent by a client.

For more information, see:

ASP.NET Core Blazor environments


Response caching in ASP.NET Core
Call JavaScript functions from .NET
methods in ASP.NET Core Blazor
Article • 06/16/2023

This article explains how to invoke JavaScript (JS) functions from .NET.

For information on how to call .NET methods from JS, see Call .NET methods from
JavaScript functions in ASP.NET Core Blazor.

IJSRuntime is registered by the Blazor framework. To call into JS from .NET, inject the
IJSRuntime abstraction and call one of the following methods:

IJSRuntime.InvokeAsync
JSRuntimeExtensions.InvokeAsync
JSRuntimeExtensions.InvokeVoidAsync

For the preceding .NET methods that invoke JS functions:

The function identifier ( String ) is relative to the global scope ( window ). To call
window.someScope.someFunction , the identifier is someScope.someFunction . There's
no need to register the function before it's called.
Pass any number of JSON-serializable arguments in Object[] to a JS function.
The cancellation token ( CancellationToken ) propagates a notification that
operations should be canceled.
TimeSpan represents a time limit for a JS operation.
The TValue return type must also be JSON serializable. TValue should match the
.NET type that best maps to the JSON type returned.
A JS Promise is returned for InvokeAsync methods. InvokeAsync unwraps the
Promise and returns the value awaited by the Promise .

For Blazor apps with prerendering enabled, calling into JS isn't possible during
prerendering. For more information, see the Prerendering section.

The following example is based on TextDecoder , a JS-based decoder. The example


demonstrates how to invoke a JS function from a C# method that offloads a
requirement from developer code to an existing JS API. The JS function accepts a byte
array from a C# method, decodes the array, and returns the text to the component for
display.

HTML
<script>
window.convertArray = (win1251Array) => {
var win1251decoder = new TextDecoder('windows-1251');
var bytes = new Uint8Array(win1251Array);
var decodedArray = win1251decoder.decode(bytes);
console.log(decodedArray);
return decodedArray;
};
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

The following CallJsExample1 component:

Invokes the convertArray JS function with InvokeAsync when selecting a button


( Convert Array ).
After the JS function is called, the passed array is converted into a string. The string
is returned to the component for display ( text ).

Pages/CallJsExample1.razor :

razor

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
<button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
@text
</p>

<p>
Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>:
<a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
<a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on
IMDB</a>
</p>

@code {
private MarkupString text;
private uint[] quoteArray =
new uint[]
{
60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112,
32,
116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85,
110,
105, 118, 101, 114, 115, 101, 10, 10,
};

private async Task ConvertArray()


{
text = new(await JS.InvokeAsync<string>("convertArray",
quoteArray));
}
}

JavaScript API restricted to user gestures


This section only applies to Blazor Server apps.

Some browser JavaScript (JS) APIs can only be executed in the context of a user gesture,
such as using the Fullscreen API (MDN documentation) . These APIs can't be called
through the JS interop mechanism in Blazor Server apps because UI event handling is
performed asynchronously and generally no longer in the context of the user gesture.
The app must handle the UI event completely in JavaScript, so use onclick instead of
Blazor's @onclick directive attribute.

Invoke JavaScript functions without reading a


returned value ( InvokeVoidAsync )
Use InvokeVoidAsync when:

.NET isn't required to read the result of a JavaScript (JS) call.


JS functions return void(0)/void 0 or undefined .

Provide a displayTickerAlert1 JS function. The function is called with InvokeVoidAsync


and doesn't return a value:

HTML

<script>
window.displayTickerAlert1 = (symbol, price) => {
alert(`${symbol}: $${price}!`);
};
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

Component ( .razor ) example ( InvokeVoidAsync )


TickerChanged calls the handleTickerChanged1 method in the following CallJsExample2

component.

Pages/CallJsExample2.razor :

razor

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
<button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)


{
<p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
private Random r = new();
private string? stockSymbol;
private decimal price;

private async Task SetStock()


{
stockSymbol =
$"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
price = r.Next(1, 101);
await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
}
}
Class ( .cs ) example ( InvokeVoidAsync )
JsInteropClasses1.cs :

C#

using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable


{
private readonly IJSRuntime js;

public JsInteropClasses1(IJSRuntime js)


{
this.js = js;
}

public async ValueTask TickerChanged(string symbol, decimal price)


{
await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
}

public void Dispose()


{
}
}

TickerChanged calls the handleTickerChanged1 method in the following CallJsExample3


component.

Pages/CallJsExample3.razor :

razor

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
<button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)


{
<p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
private JsInteropClasses1? jsClass;

protected override void OnInitialized()


{
jsClass = new(JS);
}

private async Task SetStock()


{
if (jsClass is not null)
{
stockSymbol =
$"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0,
26))}";
price = r.Next(1, 101);
await jsClass.TickerChanged(stockSymbol, price);
}
}

public void Dispose() => jsClass?.Dispose();


}

Invoke JavaScript functions and read a returned


value ( InvokeAsync )
Use InvokeAsync when .NET should read the result of a JavaScript (JS) call.

Provide a displayTickerAlert2 JS function. The following example returns a string for


display by the caller:

HTML

<script>
window.displayTickerAlert2 = (symbol, price) => {
if (price < 20) {
alert(`${symbol}: $${price}!`);
return "User alerted in the browser.";
} else {
return "User NOT alerted.";
}
};
</script>

7 Note
For general guidance on JS location and our recommendations for production
apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

Component ( .razor ) example ( InvokeAsync )


TickerChanged calls the handleTickerChanged2 method and displays the returned string

in the following CallJsExample4 component.

Pages/CallJsExample4.razor :

razor

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
<button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)


{
<p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)


{
<p>@result</p>
}

@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
private string? result;

private async Task SetStock()


{
stockSymbol =
$"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
price = r.Next(1, 101);
var interopResult =
await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol,
price);
result = $"Result of TickerChanged call for {stockSymbol} at " +
$"{price.ToString("c")}: {interopResult}";
}
}
Class ( .cs ) example ( InvokeAsync )
JsInteropClasses2.cs :

C#

using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable


{
private readonly IJSRuntime js;

public JsInteropClasses2(IJSRuntime js)


{
this.js = js;
}

public async ValueTask<string> TickerChanged(string symbol, decimal


price)
{
return await js.InvokeAsync<string>("displayTickerAlert2", symbol,
price);
}

public void Dispose()


{
}
}

TickerChanged calls the handleTickerChanged2 method and displays the returned string

in the following CallJsExample5 component.

Pages/CallJsExample5.razor :

razor

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
<button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)


{
<p>@stockSymbol price: @price.ToString("c")</p>
}
@if (result is not null)
{
<p>@result</p>
}

@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
private JsInteropClasses2? jsClass;
private string? result;

protected override void OnInitialized()


{
jsClass = new(JS);
}

private async Task SetStock()


{
if (jsClass is not null)
{
stockSymbol =
$"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0,
26))}";
price = r.Next(1, 101);
var interopResult = await jsClass.TickerChanged(stockSymbol,
price);
result = $"Result of TickerChanged call for {stockSymbol} at " +
$"{price.ToString("c")}: {interopResult}";
}
}

public void Dispose() => jsClass?.Dispose();


}

Dynamic content generation scenarios


For dynamic content generation with BuildRenderTree, use the [Inject] attribute:

razor

[Inject]
IJSRuntime JS { get; set; }

Prerendering
This section applies to Blazor Server and hosted Blazor WebAssembly apps that prerender
Razor components. Prerendering is covered in Prerender and integrate ASP.NET Core
Razor components.

While an app is prerendering, certain actions, such as calling into JavaScript (JS), aren't
possible.

For the following example, the setElementText1 function is placed inside the <head>
element. The function is called with JSRuntimeExtensions.InvokeVoidAsync and doesn't
return a value.

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

HTML

<script>
window.setElementText1 = (element, text) => element.innerText = text;
</script>

2 Warning

The preceding example modifies the Document Object Model (DOM) directly for
demonstration purposes only. Directly modifying the DOM with JS isn't
recommended in most scenarios because JS can interfere with Blazor's change
tracking. For more information, see ASP.NET Core Blazor JavaScript
interoperability (JS interop).

The OnAfterRender{Async} lifecycle event isn't called during the prerendering process
on the server. Override the OnAfterRender{Async} method to delay JS interop calls until
after the component is rendered and interactive on the client after prerendering.

Pages/PrerenderedInterop1.razor :

razor

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
private ElementReference divElement;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync(
"setElementText1", divElement, "Text after render");
}
}
}

7 Note

The preceding example pollutes the client with global methods. For a better
approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

JavaScript

export setElementText1 = (element, text) => element.innerText = text;

The following component demonstrates how to use JS interop as part of a component's


initialization logic in a way that's compatible with prerendering. The component shows
that it's possible to trigger a rendering update from inside OnAfterRenderAsync. The
developer must be careful to avoid creating an infinite loop in this scenario.

For the following example, the setElementText2 function is placed inside the <head>
element. The function is called with IJSRuntime.InvokeAsync and returns a value.

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

HTML

<script>
window.setElementText2 = (element, text) => {
element.innerText = text;
return text;
};
</script>
2 Warning

The preceding example modifies the Document Object Model (DOM) directly for
demonstration purposes only. Directly modifying the DOM with JS isn't
recommended in most scenarios because JS can interfere with Blazor's change
tracking. For more information, see ASP.NET Core Blazor JavaScript
interoperability (JS interop).

Where JSRuntime.InvokeAsync is called, the ElementReference is only used in


OnAfterRenderAsync and not in any earlier lifecycle method because there's no JS
element until after the component is rendered.

StateHasChanged is called to rerender the component with the new state obtained from
the JS interop call (for more information, see ASP.NET Core Razor component
rendering). The code doesn't create an infinite loop because StateHasChanged is only
called when data is null .

Pages/PrerenderedInterop2.razor :

razor

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
Get value via JS interop call:
<strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
private string? data;
private ElementReference divElement;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender && data == null)
{
data = await JS.InvokeAsync<string>(
"setElementText2", divElement, "Hello from interop call!");

StateHasChanged();
}
}
}

7 Note

The preceding example pollutes the client with global methods. For a better
approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

JavaScript

export setElementText2 = (element, text) => {


element.innerText = text;
return text;
};

Synchronous JS interop in Blazor WebAssembly


apps
This section only applies to Blazor WebAssembly apps.

JS interop calls are asynchronous by default, regardless of whether the called code is
synchronous or asynchronous. Calls are asynchronous by default to ensure that
components are compatible across both Blazor hosting models, Blazor Server and Blazor
WebAssembly. On Blazor Server, all JS interop calls must be asynchronous because
they're sent over a network connection.

If you know for certain that your app only ever runs on Blazor WebAssembly, you can
choose to make synchronous JS interop calls. This has slightly less overhead than
making asynchronous calls and can result in fewer render cycles because there's no
intermediate state while awaiting results.

To make a synchronous call from .NET to JavaScript in a Blazor WebAssembly app, cast
IJSRuntime to IJSInProcessRuntime to make the JS interop call:
razor

@inject IJSRuntime JS

...

@code {
protected override void HandleSomeEvent()
{
var jsInProcess = (IJSInProcessRuntime)JS;
var value = jsInProcess.Invoke<string>
("javascriptFunctionIdentifier");
}
}

When working with IJSObjectReference in ASP.NET Core 5.0 or later Blazor


WebAssembly apps, you can use IJSInProcessObjectReference synchronously instead.
IJSInProcessObjectReference implements IAsyncDisposable/IDisposable and should be
disposed for garbage collection to prevent a memory leak, as the following example
demonstrates:

razor

...

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
...
private IJSInProcessObjectReference? module;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
module = await JS.InvokeAsync<IJSInProcessObjectReference>
("import",
"./scripts.js");
}
}

...

async ValueTask IAsyncDisposable.DisposeAsync()


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

JavaScript location
Load JavaScript (JS) code using any of approaches described by the JavaScript (JS)
interoperability (interop) overview article:

Load a script in <head> markup (Not generally recommended)


Load a script in <body> markup
Load a script from an external JavaScript file (.js) collocated with a component
Load a script from an external JavaScript file (.js)
Inject a script before or after Blazor starts

For information on isolating scripts in JS modules , see the JavaScript isolation in


JavaScript modules section.

2 Warning

Don't place a <script> tag in a component file ( .razor ) because the <script> tag
can't be updated dynamically.

JavaScript isolation in JavaScript modules


Blazor enables JavaScript (JS) isolation in standard JavaScript modules (ECMAScript
specification ). JavaScript module loading works the same way in Blazor as it does for
other types of web apps, and you're free to customize how modules are defined in your
app. For a guide on how to use JavaScript modules, see MDN Web Docs: JavaScript
modules .

JS isolation provides the following benefits:

Imported JS no longer pollutes the global namespace.


Consumers of a library and components aren't required to import the related JS.

Dynamic import with the import() operator is supported with ASP.NET Core and
Blazor:

JavaScript

if ({CONDITION}) import("/additionalModule.js");
In the preceding example, the {CONDITION} placeholder represents a conditional check
to determine if the module should be loaded.

For browser compatibility, see Can I use: JavaScript modules: dynamic import .

For example, the following JS module exports a JS function for showing a browser
window prompt . Place the following JS code in an external JS file.

wwwroot/scripts.js :

JavaScript

export function showPrompt(message) {


return prompt(message, 'Type anything here');
}

Add the preceding JS module to an app or class library as a static web asset in the
wwwroot folder and then import the module into the .NET code by calling InvokeAsync

on the IJSRuntime instance.

IJSRuntime imports the module as an IJSObjectReference, which represents a reference


to a JS object from .NET code. Use the IJSObjectReference to invoke exported JS
functions from the module.

Pages/CallJsExample6.razor :

razor

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
<button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
@result
</p>

@code {
private IJSObjectReference? module;
private string? result;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>("import",
"./scripts.js");
}
}

private async Task TriggerPrompt()


{
result = await Prompt("Provide some text");
}

public async ValueTask<string?> Prompt(string message) =>


module is not null ?
await module.InvokeAsync<string>("showPrompt", message) : null;

async ValueTask IAsyncDisposable.DisposeAsync()


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

In the preceding example:

By convention, the import identifier is a special identifier used specifically for


importing a JS module.
Specify the module's external JS file using its stable static web asset path:
./{SCRIPT PATH AND FILE NAME (.js)} , where:

The path segment for the current directory ( ./ ) is required in order to create
the correct static asset path to the JS file.
The {SCRIPT PATH AND FILE NAME (.js)} placeholder is the path and file name
under wwwroot .
Disposes the IJSObjectReference for garbage collection in
IAsyncDisposable.DisposeAsync.

Dynamically importing a module requires a network request, so it can only be achieved


asynchronously by calling InvokeAsync.

IJSInProcessObjectReference represents a reference to a JS object whose functions can

be invoked synchronously in Blazor WebAssembly apps. For more information, see the
Synchronous JS interop in Blazor WebAssembly apps section.

7 Note
When the external JS file is supplied by a Razor class library, specify the module's
JS file using its stable static web asset path: ./_content/{PACKAGE ID}/{SCRIPT PATH
AND FILE NAME (.js)} :

The path segment for the current directory ( ./ ) is required in order to create
the correct static asset path to the JS file.
The {PACKAGE ID} placeholder is the library's package ID. The package ID
defaults to the project's assembly name if <PackageId> isn't specified in the
project file. In the following example, the library's assembly name is
ComponentLibrary and the library's project file doesn't specify <PackageId> .

The {SCRIPT PATH AND FILE NAME (.js)} placeholder is the path and file name
under wwwroot . In the following example, the external JS file ( script.js ) is
placed in the class library's wwwroot folder.

C#

var module = await js.InvokeAsync<IJSObjectReference>(


"import", "./_content/ComponentLibrary/scripts.js");

For more information, see Consume ASP.NET Core Razor components from a
Razor class library (RCL).

Throughout the Blazor documentation, examples use the .js file extension for module
files, not the newer .mjs file extension (RFC 9239) . Our documentation continues to
use the .js file extension for the same reasons the Mozilla Foundation's documentation
continues to use the .js file extension. For more information, see Aside — .mjs versus
.js (MDN documentation) .

Capture references to elements


Some JavaScript (JS) interop scenarios require references to HTML elements. For
example, a UI library may require an element reference for initialization, or you might
need to call command-like APIs on an element, such as click or play .

Capture references to HTML elements in a component using the following approach:

Add an @ref attribute to the HTML element.


Define a field of type ElementReference whose name matches the value of the
@ref attribute.
The following example shows capturing a reference to the username <input> element:

razor

<input @ref="username" ... />

@code {
private ElementReference username;
}

2 Warning

Only use an element reference to mutate the contents of an empty element that
doesn't interact with Blazor. This scenario is useful when a third-party API supplies
content to the element. Because Blazor doesn't interact with the element, there's
no possibility of a conflict between Blazor's representation of the element and the
Document Object Model (DOM).

In the following example, it's dangerous to mutate the contents of the unordered
list ( ul ) using MyList via JS interop because Blazor interacts with the DOM to
populate this element's list items ( <li> ) from the Todos object:

razor

<ul @ref="MyList">
@foreach (var item in Todos)
{
<li>@item.Text</li>
}
</ul>

Using the MyList element reference to merely read DOM content or trigger an
event is supported.

If JS interop mutates the contents of element MyList and Blazor attempts to apply
diffs to the element, the diffs won't match the DOM. Modifying the contents of the
list via JS interop with the MyList element reference is not supported.

For more information, see ASP.NET Core Blazor JavaScript interoperability (JS
interop).

An ElementReference is passed through to JS code via JS interop. The JS code receives


an HTMLElement instance, which it can use with normal DOM APIs. For example, the
following code defines a .NET extension method ( TriggerClickEvent ) that enables
sending a mouse click to an element.

The JS function clickElement creates a click event on the passed HTML element
( element ):

JavaScript

window.interopFunctions = {
clickElement : function (element) {
element.click();
}
}

To call a JS function that doesn't return a value, use


JSRuntimeExtensions.InvokeVoidAsync. The following code triggers a client-side click
event by calling the preceding JS function with the captured ElementReference:

razor

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
Trigger click event on <code>Example Button</code>
</button>

@code {
private ElementReference exampleButton;

public async Task TriggerClick()


{
await JS.InvokeVoidAsync(
"interopFunctions.clickElement", exampleButton);
}
}

To use an extension method, create a static extension method that receives the
IJSRuntime instance:

C#

public static async Task TriggerClickEvent(this ElementReference elementRef,


IJSRuntime js)
{
await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}
The clickElement method is called directly on the object. The following example
assumes that the TriggerClickEvent method is available from the JsInteropClasses
namespace:

razor

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
Trigger click event on <code>Example Button</code>
</button>

@code {
private ElementReference exampleButton;

public async Task TriggerClick()


{
await exampleButton.TriggerClickEvent(JS);
}
}

) Important

The exampleButton variable is only populated after the component is rendered. If


an unpopulated ElementReference is passed to JS code, the JS code receives a
value of null . To manipulate element references after the component has finished
rendering, use the OnAfterRenderAsync or OnAfterRender component lifecycle
methods.

When working with generic types and returning a value, use ValueTask<TResult>:

C#

public static ValueTask<T> GenericMethod<T>(this ElementReference


elementRef,
IJSRuntime js)
{
return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

The {JAVASCRIPT FUNCTION} placeholder is the JS function identifier.


GenericMethod is called directly on the object with a type. The following example

assumes that the GenericMethod is available from the JsInteropClasses namespace:

razor

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
returnValue: @returnValue
</p>

@code {
private ElementReference username;
private string? returnValue;

private async Task OnClickMethod()


{
returnValue = await username.GenericMethod<string>(JS);
}
}

Reference elements across components


An ElementReference can't be passed between components because:

The instance is only guaranteed to exist after the component is rendered, which is
during or after a component's OnAfterRender/OnAfterRenderAsync method
executes.
An ElementReference is a struct, which can't be passed as a component parameter.

For a parent component to make an element reference available to other components,


the parent component can:

Allow child components to register callbacks.


Invoke the registered callbacks during the OnAfterRender event with the passed
element reference. Indirectly, this approach allows child components to interact
with the parent's element reference.

HTML

<style>
.red { color: red }
</style>

HTML

<script>
function setElementClass(element, className) {
var myElement = element;
myElement.classList.add(className);
}
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

Pages/CallJsExample7.razor (parent component):

razor

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />

Pages/CallJsExample7.razor.cs :

C#

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages;

public partial class CallJsExample7 :


ComponentBase, IObservable<ElementReference>, IDisposable
{
private bool disposing;
private IList<IObserver<ElementReference>> subscriptions =
new List<IObserver<ElementReference>>();
private ElementReference title;
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);

foreach (var subscription in subscriptions)


{
try
{
subscription.OnNext(title);
}
catch (Exception)
{
throw;
}
}
}

public void Dispose()


{
disposing = true;

foreach (var subscription in subscriptions)


{
try
{
subscription.OnCompleted();
}
catch (Exception)
{
}
}

subscriptions.Clear();
}

public IDisposable Subscribe(IObserver<ElementReference> observer)


{
if (disposing)
{
throw new InvalidOperationException("Parent being disposed");
}

subscriptions.Add(observer);

return new Subscription(observer, this);


}

private class Subscription : IDisposable


{
public Subscription(IObserver<ElementReference> observer,
CallJsExample7 self)
{
Observer = observer;
Self = self;
}
public IObserver<ElementReference> Observer { get; }
public CallJsExample7 Self { get; }

public void Dispose()


{
Self.subscriptions.Remove(Observer);
}
}
}

In the preceding example, the namespace of the app is BlazorSample with components
in the Pages folder. If testing the code locally, update the namespace.

Shared/SurveyPrompt.razor (child component):

razor

<div class="alert alert-secondary mt-4">


<span class="oi oi-pencil me-2" aria-hidden="true"></span>
<strong>@Title</strong>

<span class="text-nowrap">
Please take our
<a target="_blank" class="font-weight-bold link-dark"
href="https://go.microsoft.com/fwlink/?linkid=2186157">brief survey</a>
</span>
and tell us what you think.
</div>

@code {
// Demonstrates how a parent component can supply parameters
[Parameter]
public string? Title { get; set; }
}

Shared/SurveyPrompt.razor.cs :

C#

using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared;

public partial class SurveyPrompt :


ComponentBase, IObserver<ElementReference>, IDisposable
{
private IDisposable? subscription = null;

[Parameter]
public IObservable<ElementReference>? Parent { get; set; }

protected override void OnParametersSet()


{
base.OnParametersSet();

subscription?.Dispose();
subscription =
Parent is not null ? Parent.Subscribe(this) : null;
}

public void OnCompleted()


{
subscription = null;
}

public void OnError(Exception error)


{
subscription = null;
}

public void OnNext(ElementReference value)


{
JS.InvokeAsync<object>(
"setElementClass", new object[] { value, "red" });
}

public void Dispose()


{
subscription?.Dispose();
}
}

In the preceding example, the namespace of the app is BlazorSample with shared
components in the Shared folder. If testing the code locally, update the namespace.

Harden JavaScript interop calls


This section primarily applies to Blazor Server apps, but Blazor WebAssembly apps may
also set JS interop timeouts if conditions warrant it.

In Blazor Server apps, JavaScript (JS) interop may fail due to networking errors and
should be treated as unreliable. By default, Blazor Server apps use a one minute timeout
for JS interop calls. If an app can tolerate a more aggressive timeout, set the timeout
using one of the following approaches.

Set a global timeout in the Program.cs with CircuitOptions.JSInteropDefaultCallTimeout:

C#
builder.Services.AddServerSideBlazor(
options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

The {TIMEOUT} placeholder is a TimeSpan (for example, TimeSpan.FromSeconds(80) ).

Set a per-invocation timeout in component code. The specified timeout overrides the
global timeout set by JSInteropDefaultCallTimeout:

C#

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1"


});

In the preceding example:

The {TIMEOUT} placeholder is a TimeSpan (for example, TimeSpan.FromSeconds(80) ).


The {ID} placeholder is the identifier for the function to invoke. For example, the
value someScope.someFunction invokes the function
window.someScope.someFunction .

Although a common cause of JS interop failures are network failures in Blazor Server
apps, per-invocation timeouts can be set for JS interop calls in Blazor WebAssembly
apps. Although no SignalR circuit exists in a Blazor WebAssembly app, JS interop calls
might fail for other reasons that apply in Blazor WebAssembly apps.

For more information on resource exhaustion, see Threat mitigation guidance for
ASP.NET Core Blazor Server.

Avoid circular object references


Objects that contain circular references can't be serialized on the client for either:

.NET method calls.


JavaScript method calls from C# when the return type has circular references.

JavaScript libraries that render UI


Sometimes you may wish to use JavaScript (JS) libraries that produce visible user
interface elements within the browser Document Object Model (DOM). At first glance,
this might seem difficult because Blazor's diffing system relies on having control over
the tree of DOM elements and runs into errors if some external code mutates the DOM
tree and invalidates its mechanism for applying diffs. This isn't a Blazor-specific
limitation. The same challenge occurs with any diff-based UI framework.

Fortunately, it's straightforward to embed externally-generated UI within a Razor


component UI reliably. The recommended technique is to have the component's code
( .razor file) produce an empty element. As far as Blazor's diffing system is concerned,
the element is always empty, so the renderer does not recurse into the element and
instead leaves its contents alone. This makes it safe to populate the element with
arbitrary externally-managed content.

The following example demonstrates the concept. Within the if statement when
firstRender is true , interact with unmanagedElement outside of Blazor using JS interop.

For example, call an external JS library to populate the element. Blazor leaves the
element's contents alone until this component is removed. When the component is
removed, the component's entire DOM subtree is also removed.

razor

<h1>Hello! This is a Razor component rendered at @DateTime.Now</h1>

<div @ref="unmanagedElement"></div>

@code {
private ElementReference unmanagedElement;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
...
}
}
}

Consider the following example that renders an interactive map using open-source
Mapbox APIs .

The following JS module is placed into the app or made available from a Razor class
library.

7 Note

To create the Mapbox map, obtain an access token from Mapbox Sign in and
provide it where the {ACCESS TOKEN} appears in the following code.
wwwroot/mapComponent.js :

JavaScript

import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';

mapboxgl.accessToken = '{ACCESS TOKEN}';

export function addMapToElement(element) {


return new mapboxgl.Map({
container: element,
style: 'mapbox://styles/mapbox/streets-v11',
center: [-74.5, 40],
zoom: 9
});
}

export function setMapCenter(map, latitude, longitude) {


map.setCenter([longitude, latitude]);
}

To produce correct styling, add the following stylesheet tag to the host HTML page.

Add the following <link> element to the <head> element markup (location of <head>
content):

HTML

<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css"
rel="stylesheet" />

Pages/CallJsExample8.razor :

razor

@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol,


UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo,
Japan</button>

@code
{
private ElementReference mapElement;
private IJSObjectReference? mapModule;
private IJSObjectReference? mapInstance;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
mapModule = await JS.InvokeAsync<IJSObjectReference>(
"import", "./mapComponent.js");
mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
"addMapToElement", mapElement);
}
}

private async Task ShowAsync(double latitude, double longitude)


{
if (mapModule is not null && mapInstance is not null)
{
await mapModule.InvokeVoidAsync("setMapCenter", mapInstance,
latitude, longitude).AsTask();
}
}

async ValueTask IAsyncDisposable.DisposeAsync()


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

if (mapModule is not null)


{
await mapModule.DisposeAsync();
}
}
}

The preceding example produces an interactive map UI. The user:

Can drag to scroll or zoom.


Select buttons to jump to predefined locations.
In the preceding example:

The <div> with @ref="mapElement" is left empty as far as Blazor is concerned. The
mapbox-gl.js script can safely populate the element and modify its contents. Use
this technique with any JS library that renders UI. You can embed components
from a third-party JS SPA framework inside Razor components, as long as they
don't try to reach out and modify other parts of the page. It is not safe for external
JS code to modify elements that Blazor does not regard as empty.
When using this approach, bear in mind the rules about how Blazor retains or
destroys DOM elements. The component safely handles button click events and
updates the existing map instance because DOM elements are retained where
possible by default. If you were rendering a list of map elements from inside a
@foreach loop, you want to use @key to ensure the preservation of component

instances. Otherwise, changes in the list data could cause component instances to
retain the state of previous instances in an undesirable manner. For more
information, see how to use the @key directive attribute to preserve the
relationship among elements, components, and model objects.
The example encapsulates JS logic and dependencies within an ES6 module and
loads the module dynamically using the import identifier. For more information,
see JavaScript isolation in JavaScript modules.
Byte array support
Blazor supports optimized byte array JavaScript (JS) interop that avoids
encoding/decoding byte arrays into Base64. The following example uses JS interop to
pass a byte array to JavaScript.

Provide a receiveByteArray JS function. The function is called with InvokeVoidAsync and


doesn't return a value:

HTML

<script>
window.receiveByteArray = (bytes) => {
let utf8decoder = new TextDecoder();
let str = utf8decoder.decode(bytes);
return str;
};
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

Pages/CallJsExample9.razor :

razor

@page "/call-js-example-9"
@inject IJSRuntime JS

<h1>Call JS Example 9</h1>

<p>
<button @onclick="SendByteArray">Send Bytes</button>
</p>

<p>
@result
</p>

<p>
Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
<a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
<a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
private string? result;

private async Task SendByteArray()


{
var bytes = new byte[] { 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68,
0x69,
0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79,
0x2c,
0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20,
0x4e,
0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e
};

result = await JS.InvokeAsync<string>("receiveByteArray", bytes);


}
}

For information on using a byte array when calling .NET from JavaScript, see Call .NET
methods from JavaScript functions in ASP.NET Core Blazor.

Stream from .NET to JavaScript


Blazor supports streaming data directly from .NET to JavaScript. Streams are created
using a DotNetStreamReference.

DotNetStreamReference represents a .NET stream and uses the following parameters:

stream : The stream sent to JavaScript.

leaveOpen : Determines if the stream is left open after transmission. If a value isn't
provided, leaveOpen defaults to false .

In JavaScript, use an array buffer or a readable stream to receive the data:

Using an ArrayBuffer :

JavaScript

async function streamToJavaScript(streamRef) {


const data = await streamRef.arrayBuffer();
}

Using a ReadableStream :

JavaScript

async function streamToJavaScript(streamRef) {


const stream = await streamRef.stream();
}

In C# code:

C#

using var streamRef = new DotNetStreamReference(stream: {STREAM}, leaveOpen:


false);
await JS.InvokeVoidAsync("streamToJavaScript", streamRef);

In the preceding example:

The {STREAM} placeholder represents the Stream sent to JavaScript.


JS is an injected IJSRuntime instance.

Call .NET methods from JavaScript functions in ASP.NET Core Blazor covers the reverse
operation, streaming from JavaScript to .NET.

ASP.NET Core Blazor file downloads covers how to download a file in Blazor.

Catch JavaScript exceptions


To catch JS exceptions, wrap the JS interop in a try-catch block and catch a JSException.

In the following example, the nonFunction JS function doesn't exist. When the function
isn't found, the JSException is trapped with a Message that indicates the following error:

Could not find 'nonFunction' ('nonFunction' was undefined).

Pages/CallJsExample11.razor :

C#

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
<button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
@result
</p>
<p>
@errorMessage
</p>

@code {
private string? errorMessage;
private string? result;

private async Task CatchUndefinedJSFunction()


{
try
{
result = await JS.InvokeAsync<string>("nonFunction");
}
catch (JSException e)
{
errorMessage = $"Error Message: {e.Message}";
}
}
}

Abort a long-running JavaScript function


Use a JS AbortController with a CancellationTokenSource in the component to abort a
long-running JavaScript function from C# code.

The following JS Helpers class contains a simulated long-running function,


longRunningFn , to count continuously until the AbortController.signal indicates that
AbortController.abort has been called. The sleep function is for demonstration
purposes to simulate slow execution of the long-running function and wouldn't be
present in production code. When a component calls stopFn , the longRunningFn is
signalled to abort via the while loop conditional check on AbortSignal.aborted .

HTML

<script>
class Helpers {
static #controller = new AbortController();

static async #sleep(ms) {


return new Promise(resolve => setTimeout(resolve, ms));
}

static async longRunningFn() {


var i = 0;
while (!this.#controller.signal.aborted) {
i++;
console.log(`longRunningFn: ${i}`);
await this.#sleep(1000);
}
}

static stopFn() {
this.#controller.abort();
console.log('longRunningFn aborted!');
}
}

window.Helpers = Helpers;
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

The following CallJsExample12 component:

Invokes the JS function longRunningFn when the Start Task button is selected. A
CancellationTokenSource is used to manage the execution of the long-running
function. CancellationToken.Register sets a JS interop call delegate to execute the
JS function stopFn when the CancellationTokenSource.Token is cancelled.
When the Cancel Task button is selected, the CancellationTokenSource.Token is
cancelled with a call to Cancel.
The CancellationTokenSource is disposed in the Dispose method.

Pages/CallJsExample12.razor :

razor

@page "/call-js-example-12"
@inject IJSRuntime JS

<h1>Cancel long-running JS interop</h1>

<p>
<button @onclick="StartTask">Start Task</button>
<button @onclick="CancelTask">Cancel Task</button>
</p>

@code {
private CancellationTokenSource? cts;

private async Task StartTask()


{
cts = new CancellationTokenSource();
cts.Token.Register(() => JS.InvokeVoidAsync("Helpers.stopFn"));
await JS.InvokeVoidAsync("Helpers.longRunningFn");
}

private void CancelTask()


{
cts?.Cancel();
}

public void Dispose()


{
cts?.Cancel();
cts?.Dispose();
}
}

A browser's developer tools console indicates the execution of the long-running JS


function after the Start Task button is selected and when the function is aborted after
the Cancel Task button is selected:

Console

longRunningFn: 1
longRunningFn: 2
longRunningFn: 3
longRunningFn aborted!

JavaScript [JSImport] / [JSExport] interop


This section applies to Blazor WebAssembly apps.

As an alternative to interacting with JavaScript (JS) in Blazor WebAssembly apps using


Blazor's JS interop mechanism based on the IJSRuntime interface, a JS
[JSImport] / [JSExport] interop API is available to apps targeting .NET 7 or later.

For more information, see JavaScript JSImport/JSExport interop with ASP.NET Core
Blazor WebAssembly.

Unmarshalled JavaScript interop


This section applies to Blazor WebAssembly apps.

Unmarshalled interop using the IJSUnmarshalledRuntime interface is obsolete and


should be replaced with JavaScript [JSImport] / [JSExport] interop.
For more information, see JavaScript JSImport/JSExport interop with ASP.NET Core
Blazor WebAssembly.

Disposal of JavaScript interop object references


Examples throughout the JavaScript (JS) interop articles demonstrate typical object
disposal patterns:

When calling JS from .NET, as described in this article, dispose any created
IJSObjectReference/IJSInProcessObjectReference/ JSObjectReference either from
.NET or from JS to avoid leaking JS memory.

When calling .NET from JS, as described in Call .NET methods from JavaScript
functions in ASP.NET Core Blazor, dispose of a created DotNetObjectReference
either from .NET or from JS to avoid leaking .NET memory.

JS interop object references are implemented as a map keyed by an identifier on the


side of the JS interop call that creates the reference. When object disposal is initiated
from either the .NET or JS side, Blazor removes the entry from the map, and the object
can be garbage collected as long as no other strong reference to the object is present.

At a minimum, always dispose objects created on the .NET side to avoid leaking .NET
managed memory.

Document Object Model (DOM) cleanup tasks


during component disposal
Don't execute JS interop code for DOM cleanup tasks during component disposal.
Instead, use the MutationObserver pattern in JavaScript on the client for the following
reasons:

The component may have been removed from the DOM by the time your cleanup
code executes in Dispose{Async} .
In a Blazor Server app, the Blazor renderer may have been disposed by the
framework by the time your cleanup code executes in Dispose{Async} .

The MutationObserver pattern allows you to run a function when an element is


removed from the DOM.

JavaScript interop calls without a circuit


This section only applies to Blazor Server apps.

JavaScript (JS) interop calls can't be issued after a SignalR circuit is disconnected.
Without a circuit during component disposal or at any other time that a circuit doesn't
exist, the following method calls fail and log a message that the circuit is disconnected
as a JSDisconnectedException:

JS interop method calls


IJSRuntime.InvokeAsync
JSRuntimeExtensions.InvokeAsync
JSRuntimeExtensions.InvokeVoidAsync)
Dispose / DisposeAsync calls on any IJSObjectReference.

In order to avoid logging JSDisconnectedException or to log custom information, catch


the exception in a try-catch statement.

For the following component disposal example:

The component implements IAsyncDisposable.


objInstance is an IJSObjectReference.

JSDisconnectedException is caught and not logged.


Optionally, you can log custom information in the catch statement at whatever
log level you prefer. The following example doesn't log custom information
because it assumes the developer doesn't care about when or where circuits are
disconnected during component disposal.

C#

async ValueTask IAsyncDisposable.DisposeAsync()


{
try
{
if (objInstance is not null)
{
await objInstance.DisposeAsync();
}
}
catch (JSDisconnectedException)
{
}
}

If you must clean up your own JS objects or execute other JS code on the client after a
circuit is lost, use the MutationObserver pattern in JS on the client. The
MutationObserver pattern allows you to run a function when an element is removed
from the DOM.
For more information, see the following articles:

Handle errors in ASP.NET Core Blazor apps: The JavaScript interop section discusses
error handling in JS interop scenarios.
ASP.NET Core Razor component lifecycle: The Component disposal with
IDisposable and IAsyncDisposable section describes how to implement disposal

patterns in Razor components.

Additional resources
Call .NET methods from JavaScript functions in ASP.NET Core Blazor
InteropComponent.razor example (dotnet/AspNetCore GitHub repository main
branch) : The main branch represents the product unit's current development for
the next release of ASP.NET Core. To select the branch for a different release (for
example, release/5.0 ), use the Switch branches or tags dropdown list to select
the branch.
Blazor samples GitHub repository (dotnet/blazor-samples)
Handle errors in ASP.NET Core Blazor apps (JavaScript interop section)
Blazor Server threat mitigation: JavaScript functions invoked from .NET
Call .NET methods from JavaScript
functions in ASP.NET Core Blazor
Article • 06/16/2023

This article explains how to invoke .NET methods from JavaScript (JS).

For information on how to call JS functions from .NET, see Call JavaScript functions from
.NET methods in ASP.NET Core Blazor.

Invoke a static .NET method


To invoke a static .NET method from JavaScript (JS), use the JS functions:

DotNet.invokeMethodAsync (Recommended): Asynchronous for both Blazor Server


and Blazor WebAssembly apps.
DotNet.invokeMethod : Synchronous for Blazor WebAssembly apps only.

Pass in the name of the assembly containing the method, the identifier of the static .NET
method, and any arguments.

In the following example:

The {ASSEMBLY NAME} placeholder is the app's assembly name.


The {.NET METHOD ID} placeholder is the .NET method identifier.
The {ARGUMENTS} placeholder are optional, comma-separated arguments to pass to
the method, each of which must be JSON-serializable.

JavaScript

DotNet.invokeMethodAsync('{ASSEMBLY NAME}', '{.NET METHOD ID}',


{ARGUMENTS});

DotNet.invokeMethodAsync returns a JS Promise representing the result of the


operation. DotNet.invokeMethod (Blazor WebAssembly only) returns the result of the
operation.

) Important

The asynchronous function ( invokeMethodAsync ) is preferred over the synchronous


version ( invokeMethod ) to support Blazor Server scenarios.
The .NET method must be public, static, and have the [JSInvokable] attribute.

In the following example:

The {<T>} placeholder indicates the return type, which is only required for
methods that return a value.
The {.NET METHOD ID} placeholder is the method identifier.

razor

@code {
[JSInvokable]
public static Task{<T>} {.NET METHOD ID}()
{
...
}
}

7 Note

Calling open generic methods isn't supported with static .NET methods but is
supported with instance methods. For more information, see the Call .NET generic
class methods section.

In the following CallDotNetExample1 component, the ReturnArrayAsync C# method


returns an int array. The [JSInvokable] attribute is applied to the method, which makes
the method invokable by JS.

Pages/CallDotNetExample1.razor :

razor

@page "/call-dotnet-example-1"

<h1>Call .NET Example 1</h1>

<p>
<button onclick="returnArrayAsync()">
Trigger .NET static method
</button>
</p>

@code {
[JSInvokable]
public static Task<int[]> ReturnArrayAsync()
{
return Task.FromResult(new int[] { 1, 2, 3 });
}
}

The <button> element's onclick HTML attribute is JavaScript's onclick event handler
assignment for processing click events, not Blazor's @onclick directive attribute. The
returnArrayAsync JS function is assigned as the handler.

The following returnArrayAsync JS function, calls the ReturnArrayAsync .NET method of


the preceding CallDotNetExample1 component and logs the result to the browser's web
developer tools console. BlazorSample is the app's assembly name.

HTML

<script>
window.returnArrayAsync = () => {
DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
.then(data => {
console.log(data);
});
};
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

When the Trigger .NET static method button is selected, the browser's developer tools
console output displays the array data. The format of the output differs slightly among
browsers. The following output shows the format used by Microsoft Edge:

Console

Array(3) [ 1, 2, 3 ]

Pass data to a .NET method when calling the invokeMethodAsync function by passing the
data as arguments.

To demonstrate passing data to .NET, make the preceding returnArrayAsync JS function


receive a starting position when the function is called and pass the value as an argument
to the invokeMethodAsync function:

HTML

<script>
window.returnArrayAsync = (startPosition) => {
DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync',
startPosition)
.then(data => {
console.log(data);
});
};
</script>

In the CallDotNetExample1 component, change the function call to include a starting


position. The following example uses a value of 5 :

razor

<button onclick="returnArrayAsync(5)">
...
</button>

The component's invokable ReturnArrayAsync method receives the starting position and
constructs the array from it. The array is returned for logging to the console:

C#

[JSInvokable]
public static Task<int[]> ReturnArrayAsync(int startPosition)
{
return Task.FromResult(Enumerable.Range(startPosition, 3).ToArray());
}

After the app is recompiled and the browser is refreshed, the following output appears
in the browser's console when the button is selected:

Console

Array(3) [ 5, 6, 7 ]

By default, the .NET method identifier for the JS call is the .NET method name, but you
can specify a different identifier using the [JSInvokable] attribute constructor. In the
following example, DifferentMethodName is the assigned method identifier for the
ReturnArrayAsync method:
C#

[JSInvokable("DifferentMethodName")]

In the call to DotNet.invokeMethodAsync or DotNet.invokeMethod (Blazor WebAssembly


only), call DifferentMethodName to execute the ReturnArrayAsync .NET method:

DotNet.invokeMethodAsync('BlazorSample', 'DifferentMethodName');

DotNet.invokeMethod('BlazorSample', 'DifferentMethodName'); (Blazor

WebAssembly only)

7 Note

The ReturnArrayAsync method example in this section returns the result of a Task
without the use of explicit C# async and await keywords. Coding methods with
async and await is typical of methods that use the await keyword to return the
value of asynchronous operations.

ReturnArrayAsync method composed with async and await keywords:

C#

[JSInvokable]
public static async Task<int[]> ReturnArrayAsync()
{
return await Task.FromResult(new int[] { 1, 2, 3 });
}

For more information, see Asynchronous programming with async and await in
the C# guide.

Create JavaScript object and data references to


pass to .NET
Call DotNet.createJSObjectReference(jsObject) to construct a JS object reference so
that it can be passed to .NET, where jsObject is the JS Object used to create the JS
object reference. The following example passes a reference to the non-serializable
window object to .NET, which receives it in the ReceiveWindowObject C# method as an
IJSObjectReference:

JavaScript
DotNet.invokeMethodAsync('{ASSEMBLY NAME}', 'ReceiveWindowObject',
DotNet.createJSObjectReference(window));

C#

[JSInvokable]
public static void ReceiveWindowObject(IJSObjectReference objRef)
{
...
}

In the preceding example, the {ASSEMBLY NAME} placeholder is the app's namespace.

7 Note

The preceding example doesn't require disposal of the JSObjectReference , as a


reference to the window object isn't held in JS.

Maintaining a reference to a JSObjectReference requires disposing of it to avoid leaking


JS memory on the client. The following example refactors the preceding code to capture
a reference to the JSObjectReference , followed by a call to
DotNet.disposeJSObjectReference() to dispose of the reference:

JavaScript

var jsObjectReference = DotNet.createJSObjectReference(window);

DotNet.invokeMethodAsync('{ASSEMBLY NAME}', 'ReceiveWindowObject',


jsObjectReference);

DotNet.disposeJSObjectReference(jsObjectReference);

In the preceding example, the {ASSEMBLY NAME} placeholder is the app's namespace.

Call DotNet.createJSStreamReference(streamReference) to construct a JS stream


reference so that it can be passed to .NET, where streamReference is an ArrayBuffer ,
Blob , or any typed array , such as Uint8Array or Float32Array , used to create the
JS stream reference.

Invoke an instance .NET method


To invoke an instance .NET method from JavaScript (JS):
Pass the .NET instance by reference to JS by wrapping the instance in a
DotNetObjectReference and calling Create on it.

Invoke a .NET instance method from JS using invokeMethodAsync (Recommended)


or invokeMethod (Blazor WebAssembly only) from the passed
DotNetObjectReference. Pass the identifier of the instance .NET method and any
arguments. The .NET instance can also be passed as an argument when invoking
other .NET methods from JS.

In the following example:


dotNetHelper is a DotNetObjectReference.
The {.NET METHOD ID} placeholder is the .NET method identifier.
The {ARGUMENTS} placeholder are optional, comma-separated arguments to pass
to the method, each of which must be JSON-serializable.

JavaScript

dotNetHelper.invokeMethodAsync('{.NET METHOD ID}', {ARGUMENTS});

7 Note

invokeMethodAsync and invokeMethod don't accept an assembly name


parameter when invoking an instance method.

invokeMethodAsync returns a JS Promise representing the result of the operation.


invokeMethod (Blazor WebAssembly only) returns the result of the operation.

) Important

The asynchronous function ( invokeMethodAsync ) is preferred over the


synchronous version ( invokeMethod ) to support Blazor Server scenarios.

Dispose of the DotNetObjectReference.

The following sections of this article demonstrate various approaches for invoking an
instance .NET method:

Pass a DotNetObjectReference to an individual JavaScript function


Pass a DotNetObjectReference to a class with multiple JavaScript functions
Call .NET generic class methods
Class instance examples
Component instance .NET method helper class

Avoid trimming JavaScript-invokable .NET


methods
This section applies to Blazor WebAssembly apps with ahead-of-time (AOT) compilation
and runtime relinking enabled.

Several of the examples in the following sections are based on a class instance
approach, where the JavaScript-invokable .NET method marked with the [JSInvokable]
attribute is a member of a class that isn't a Razor component. When such .NET methods
are located in a Razor component, they're protected from runtime relinking/trimming. In
order to protect the .NET methods from trimming outside of Razor components,
implement the methods with the DynamicDependency attribute on the class's
constructor, as the following example demonstrates:

C#

using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;

public class ExampleClass {

[DynamicDependency(nameof(ExampleJSInvokableMethod))]
public ExampleClass()
{
}

[JSInvokable]
public string ExampleJSInvokableMethod()
{
...
}
}

For more information, see Prepare .NET libraries for trimming: DynamicDependency.

Pass a DotNetObjectReference to an individual


JavaScript function
The example in this section demonstrates how to pass a DotNetObjectReference to an
individual JavaScript (JS) function.
The following sayHello1 JS function receives a DotNetObjectReference and calls
invokeMethodAsync to call the GetHelloMessage .NET method of a component:

HTML

<script>
window.sayHello1 = (dotNetHelper) => {
return dotNetHelper.invokeMethodAsync('GetHelloMessage');
};
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.

For the following CallDotNetExample2 component:

The component has a JS-invokable .NET method named GetHelloMessage .


When the Trigger .NET instance method button is selected, the JS function
sayHello1 is called with the DotNetObjectReference.
sayHello1 :

Calls GetHelloMessage and receives the message result.


Returns the message result to the calling TriggerDotNetInstanceMethod method.
The returned message from sayHello1 in result is displayed to the user.
To avoid a memory leak and allow garbage collection, the .NET object reference
created by DotNetObjectReference is disposed in the Dispose method.

Pages/CallDotNetExample2.razor :

razor

@page "/call-dotnet-example-2"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 2</h1>

<p>
<label>
Name: <input @bind="name" />
</label>
</p>

<p>
<button @onclick="TriggerDotNetInstanceMethod">
Trigger .NET instance method
</button>
</p>

<p>
@result
</p>

@code {
private string? name;
private string? result;
private DotNetObjectReference<CallDotNetExample2>? objRef;

protected override void OnInitialized()


{
objRef = DotNetObjectReference.Create(this);
}

public async Task TriggerDotNetInstanceMethod()


{
result = await JS.InvokeAsync<string>("sayHello1", objRef);
}

[JSInvokable]
public string GetHelloMessage() => $"Hello, {name}!";

public void Dispose()


{
objRef?.Dispose();
}
}

In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.

To pass arguments to an instance method:

1. Add parameters to the .NET method invocation. In the following example, a name
is passed to the method. Add additional parameters to the list as needed.

HTML

<script>
window.sayHello2 = (dotNetHelper, name) => {
return dotNetHelper.invokeMethodAsync('GetHelloMessage', name);
};
</script>
In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.

2. Provide the parameter list to the .NET method.

Pages/CallDotNetExample3.razor :

razor

@page "/call-dotnet-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 3</h1>

<p>
<label>
Name: <input @bind="name" />
</label>
</p>

<p>
<button @onclick="TriggerDotNetInstanceMethod">
Trigger .NET instance method
</button>
</p>

<p>
@result
</p>

@code {
private string? name;
private string? result;
private DotNetObjectReference<CallDotNetExample3>? objRef;

protected override void OnInitialized()


{
objRef = DotNetObjectReference.Create(this);
}

public async Task TriggerDotNetInstanceMethod()


{
result = await JS.InvokeAsync<string>("sayHello2", objRef, name);
}

[JSInvokable]
public string GetHelloMessage(string passedName) => $"Hello,
{passedName}!";

public void Dispose()


{
objRef?.Dispose();
}
}

In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.

Pass a DotNetObjectReference to a class with


multiple JavaScript functions
The example in this section demonstrates how to pass a DotNetObjectReference to a
JavaScript (JS) class with multiple functions.

Create and pass a DotNetObjectReference from the OnAfterRenderAsync lifecycle


method to a JS class for multiple functions to use. Make sure that the .NET code
disposes of the DotNetObjectReference, as the following example shows.

In the following CallDotNetExampleOneHelper component, the Trigger JS function


buttons call JS functions by setting the JS onclick property, not Blazor's @onclick
directive attribute.

Pages/CallDotNetExampleOneHelper.razor :

C#

@page "/call-dotnet-example-one-helper"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET Example</PageTitle>

<h1>Pass <code>DotNetObjectReference</code> to a JavaScript class</h1>

<p>
<label>
Message: <input @bind="name" />
</label>
</p>

<p>
<button onclick="GreetingHelpers.sayHello()">
Trigger JS function <code>sayHello</code>
</button>
</p>

<p>
<button onclick="GreetingHelpers.welcomeVisitor()">
Trigger JS function <code>welcomeVisitor</code>
</button>
</p>

@code {
private string? name;
private DotNetObjectReference<CallDotNetExampleOneHelper>? dotNetHelper;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
dotNetHelper = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("GreetingHelpers.setDotNetHelper",
dotNetHelper);
}
}

[JSInvokable]
public string GetHelloMessage() => $"Hello, {name}!";

[JSInvokable]
public string GetWelcomeMessage() => $"Welcome, {name}!";

public void Dispose()


{
dotNetHelper?.Dispose();
}
}

In the preceding example:

JS is an injected IJSRuntime instance. IJSRuntime is registered by the Blazor

framework.
The variable name dotNetHelper is arbitrary and can be changed to any preferred
name.
The component must explicitly dispose of the DotNetObjectReference to permit
garbage collection and prevent a memory leak.

HTML

<script>
class GreetingHelpers {
static dotNetHelper;

static setDotNetHelper(value) {
GreetingHelpers.dotNetHelper = value;
}

static async sayHello() {


const msg =
await
GreetingHelpers.dotNetHelper.invokeMethodAsync('GetHelloMessage');
alert(`Message from .NET: "${msg}"`);
}

static async welcomeVisitor() {


const msg =
await
GreetingHelpers.dotNetHelper.invokeMethodAsync('GetWelcomeMessage');
alert(`Message from .NET: "${msg}"`);
}
}

window.GreetingHelpers = GreetingHelpers;
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

In the preceding example:

The GreetingHelpers class is added to the window object to globally define the
class, which permits Blazor to locate the class for JS interop.
The variable name dotNetHelper is arbitrary and can be changed to any preferred
name.

Call .NET generic class methods


JavaScript (JS) functions can call .NET generic class methods, where a JS function calls a
.NET method of a generic class.

In the following generic type class ( GenericType<TValue> ):

The class has a single type parameter ( TValue ) with a single generic Value
property.
The class has two non-generic methods marked with the [JSInvokable] attribute,
each with a generic type parameter named newValue :
Update synchronously updates the value of Value from newValue .

UpdateAsync asynchronously updates the value of Value from newValue after


creating an awaitable task with Task.Yield that asynchronously yields back to the
current context when awaited.
Each of the class methods write the type of TValue and the value of Value to the
console. Writing to the console is only for demonstration purposes. Production
apps usually avoid writing to the console in favor of app logging. For more
information, see ASP.NET Core Blazor logging and Logging in .NET Core and
ASP.NET Core.

7 Note

Open generic types and methods don't specify types for type placeholders.
Conversely, closed generics supply types for all type placeholders. The examples in
this section demonstrate closed generics, but invoking JS interop instance methods
with open generics is supported. Use of open generics is not supported for static
.NET method invocations, which were described earlier in this article.

For more information, see the following articles:

Generic classes and methods (C# documentation)


Generic Classes (C# Programming Guide)
Generics in .NET (.NET documentation)

GenericType.cs :

C#

using Microsoft.JSInterop;

public class GenericType<TValue>


{
public TValue? Value { get; set; }

[JSInvokable]
public void Update(TValue newValue)
{
Value = newValue;

Console.WriteLine($"Update: GenericType<{typeof(TValue)}>:
{Value}");
}

[JSInvokable]
public async void UpdateAsync(TValue newValue)
{
await Task.Yield();
Value = newValue;

Console.WriteLine($"UpdateAsync: GenericType<{typeof(TValue)}>:
{Value}");
}
}

In the following invokeMethodsAsync function:

The generic type class's Update and UpdateAsync methods are called with
arguments representing strings and numbers.
Blazor WebAssembly apps support calling .NET methods synchronously with
invokeMethod . syncInterop receives a boolean value indicating if the JS interop is

occurring in a Blazor WebAssembly app. When syncInterop is true , invokeMethod


is safely called. If the value of syncInterop is false , only the asynchronous
function invokeMethodAsync is called because the JS interop is executing in a Blazor
Server app.
For demonstration purposes, the DotNetObjectReference function call
( invokeMethod or invokeMethodAsync ), the .NET method called ( Update or
UpdateAsync ), and the argument are written to the console. The arguments use a
random number to permit matching the JS function call to the .NET method
invocation (also written to the console on the .NET side). Production code usually
doesn't write to the console, either on the client or the server. Production apps
usually rely upon app logging. For more information, see ASP.NET Core Blazor
logging and Logging in .NET Core and ASP.NET Core.

HTML

<script>
const randomInt = () => Math.floor(Math.random() * 99999);

window.invokeMethodsAsync = async (syncInterop, dotNetHelper1,


dotNetHelper2) => {
var n = randomInt();
console.log(`JS: invokeMethodAsync:Update('string ${n}')`);
await dotNetHelper1.invokeMethodAsync('Update', `string ${n}`);

n = randomInt();
console.log(`JS: invokeMethodAsync:UpdateAsync('string ${n}')`);
await dotNetHelper1.invokeMethodAsync('UpdateAsync', `string ${n}`);

if (syncInterop) {
n = randomInt();
console.log(`JS: invokeMethod:Update('string ${n}')`);
dotNetHelper1.invokeMethod('Update', `string ${n}`);
}

n = randomInt();
console.log(`JS: invokeMethodAsync:Update(${n})`);
await dotNetHelper2.invokeMethodAsync('Update', n);
n = randomInt();
console.log(`JS: invokeMethodAsync:UpdateAsync(${n})`);
await dotNetHelper2.invokeMethodAsync('UpdateAsync', n);

if (syncInterop) {
n = randomInt();
console.log(`JS: invokeMethod:Update(${n})`);
dotNetHelper2.invokeMethod('Update', n);
}
};
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

In the following GenericsExample component:

The JS function invokeMethodsAsync is called when the Invoke Interop button is


selected.
A pair of DotNetObjectReference types are created and passed to the JS function
for instances of the GenericType as a string and an int .

Pages/GenericsExample.razor :

razor

@page "/generics-example"
@using System.Runtime.InteropServices
@inject IJSRuntime JS
@implements IDisposable

<p>
<button @onclick="InvokeInterop">Invoke Interop</button>
</p>

<ul>
<li>genericType1: @genericType1?.Value</li>
<li>genericType2: @genericType2?.Value</li>
</ul>

@code {
private GenericType<string> genericType1 = new() { Value = "string 0" };
private GenericType<int> genericType2 = new() { Value = 0 };
private DotNetObjectReference<GenericType<string>>? objRef1;
private DotNetObjectReference<GenericType<int>>? objRef2;

protected override void OnInitialized()


{
objRef1 = DotNetObjectReference.Create(genericType1);
objRef2 = DotNetObjectReference.Create(genericType2);
}

public async Task InvokeInterop()


{
var syncInterop =
RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"));

await JS.InvokeVoidAsync(
"invokeMethodsAsync", syncInterop, objRef1, objRef2);
}

public void Dispose()


{
objRef1?.Dispose();
objRef2?.Dispose();
}
}

In the preceding example, JS is an injected IJSRuntime instance. IJSRuntime is


registered by the Blazor framework.

The following demonstrates typical output of the preceding example when the Invoke
Interop button is selected in a Blazor WebAssembly app:

JS: invokeMethodAsync:Update('string 37802')


.NET: Update: GenericType<System.String>: string 37802
JS: invokeMethodAsync:UpdateAsync('string 53051')
JS: invokeMethod:Update('string 26784')
.NET: Update: GenericType<System.String>: string 26784
JS: invokeMethodAsync:Update(14107)
.NET: Update: GenericType<System.Int32>: 14107
JS: invokeMethodAsync:UpdateAsync(48995)
JS: invokeMethod:Update(12872)
.NET: Update: GenericType<System.Int32>: 12872
.NET: UpdateAsync: GenericType<System.String>: string 53051
.NET: UpdateAsync: GenericType<System.Int32>: 48995

If the preceding example is implemented in a Blazor Server app, the synchronous calls
with invokeMethod are avoided. The asynchronous function ( invokeMethodAsync ) is
preferred over the synchronous version ( invokeMethod ) in Blazor Server scenarios.

Typical output of a Blazor Server app:


JS: invokeMethodAsync:Update('string 34809')
.NET: Update: GenericType<System.String>: string 34809
JS: invokeMethodAsync:UpdateAsync('string 93059')
JS: invokeMethodAsync:Update(41997)
.NET: Update: GenericType<System.Int32>: 41997
JS: invokeMethodAsync:UpdateAsync(24652)
.NET: UpdateAsync: GenericType<System.String>: string 93059
.NET: UpdateAsync: GenericType<System.Int32>: 24652

The preceding output examples demonstrate that asynchronous methods execute and
complete in an arbitrary order depending on several factors, including thread scheduling
and the speed of method execution. It isn't possible to reliably predict the order of
completion for asynchronous method calls.

Class instance examples


The following sayHello1 JS function:

Calls the GetHelloMessage .NET method on the passed DotNetObjectReference.


Returns the message from GetHelloMessage to the sayHello1 caller.

HTML

<script>
window.sayHello1 = (dotNetHelper) => {
return dotNetHelper.invokeMethodAsync('GetHelloMessage');
};
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.

The following HelloHelper class has a JS-invokable .NET method named


GetHelloMessage . When HelloHelper is created, the name in the Name property is used
to return a message from GetHelloMessage .
HelloHelper.cs :

C#

using Microsoft.JSInterop;

public class HelloHelper


{
public HelloHelper(string? name)
{
Name = name ?? "No Name";
}

public string? Name { get; set; }

[JSInvokable]
public string GetHelloMessage() => $"Hello, {Name}!";
}

The CallHelloHelperGetHelloMessage method in the following JsInteropClasses3 class


invokes the JS function sayHello1 with a new instance of HelloHelper .

JsInteropClasses3.cs :

C#

using Microsoft.JSInterop;

public class JsInteropClasses3


{
private readonly IJSRuntime js;

public JsInteropClasses3(IJSRuntime js)


{
this.js = js;
}

public async ValueTask<string> CallHelloHelperGetHelloMessage(string?


name)
{
using var objRef = DotNetObjectReference.Create(new
HelloHelper(name));
return await js.InvokeAsync<string>("sayHello1", objRef);
}
}

To avoid a memory leak and allow garbage collection, the .NET object reference created
by DotNetObjectReference is disposed when the object reference goes out of scope
with using var syntax.
When the Trigger .NET instance method button is selected in the following
CallDotNetExample4 component, JsInteropClasses3.CallHelloHelperGetHelloMessage is
called with the value of name .

Pages/CallDotNetExample4.razor :

razor

@page "/call-dotnet-example-4"
@inject IJSRuntime JS

<h1>Call .NET Example 4</h1>

<p>
<label>
Name: <input @bind="name" />
</label>
</p>

<p>
<button @onclick="TriggerDotNetInstanceMethod">
Trigger .NET instance method
</button>
</p>

<p>
@result
</p>

@code {
private string? name;
private string? result;
private JsInteropClasses3? jsInteropClasses;

protected override void OnInitialized()


{
jsInteropClasses = new JsInteropClasses3(JS);
}

private async Task TriggerDotNetInstanceMethod()


{
if (jsInteropClasses is not null)
{
result = await
jsInteropClasses.CallHelloHelperGetHelloMessage(name);
}
}
}

The following image shows the rendered component with the name Amy Pond in the
Name field. After the button is selected, Hello, Amy Pond! is displayed in the UI:
The preceding pattern shown in the JsInteropClasses3 class can also be implemented
entirely in a component.

Pages/CallDotNetExample5.razor :

razor

@page "/call-dotnet-example-5"
@inject IJSRuntime JS

<h1>Call .NET Example 5</h1>

<p>
<label>
Name: <input @bind="name" />
</label>
</p>

<p>
<button @onclick="TriggerDotNetInstanceMethod">
Trigger .NET instance method
</button>
</p>

<p>
@result
</p>

@code {
private string? name;
private string? result;

public async Task TriggerDotNetInstanceMethod()


{
using var objRef = DotNetObjectReference.Create(new
HelloHelper(name));
result = await JS.InvokeAsync<string>("sayHello1", objRef);
}
}

To avoid a memory leak and allow garbage collection, the .NET object reference created
by DotNetObjectReference is disposed when the object reference goes out of scope
with using var syntax.

The output displayed by the CallDotNetExample5 component is Hello, Amy Pond! when
the name Amy Pond is provided in the name field.

In the preceding CallDotNetExample5 component, the .NET object reference is disposed.


If a class or component doesn't dispose the DotNetObjectReference, dispose it from the
client by calling dispose on the passed DotNetObjectReference:

JavaScript

window.{JS FUNCTION NAME} = (dotNetHelper) => {


dotNetHelper.invokeMethodAsync('{.NET METHOD ID}');
dotNetHelper.dispose();
}

In the preceding example:

The {JS FUNCTION NAME} placeholder is the JS function's name.


The variable name dotNetHelper is arbitrary and can be changed to any preferred
name.
The {.NET METHOD ID} placeholder is the .NET method identifier.

Component instance .NET method helper class


A helper class can invoke a .NET instance method as an Action. Helper classes are useful
in the following scenarios:

When several components of the same type are rendered on the same page.
In Blazor Server apps with multiple users concurrently using the same component.

In the following example:

The CallDotNetExample6 component contains several ListItem1 components,


which is a shared component in the app's Shared folder.
Each ListItem1 component is composed of a message and a button.
When a ListItem1 component button is selected, that ListItem1 's UpdateMessage
method changes the list item text and hides the button.

The following MessageUpdateInvokeHelper class maintains a JS-invokable .NET method,


UpdateMessageCaller , to invoke the Action specified when the class is instantiated.

MessageUpdateInvokeHelper.cs :

C#

using Microsoft.JSInterop;

public class MessageUpdateInvokeHelper


{
private Action action;

public MessageUpdateInvokeHelper(Action action)


{
this.action = action;
}

[JSInvokable]
public void UpdateMessageCaller()
{
action.Invoke();
}
}

The following updateMessageCaller JS function invokes the UpdateMessageCaller .NET


method.

HTML

<script>
window.updateMessageCaller = (dotNetHelper) => {
dotNetHelper.invokeMethodAsync('UpdateMessageCaller');
dotNetHelper.dispose();
}
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).
In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.

The following ListItem1 component is a shared component that can be used any
number of times in a parent component and creates list items ( <li>...</li> ) for an
HTML list ( <ul>...</ul> or <ol>...</ol> ). Each ListItem1 component instance
establishes an instance of MessageUpdateInvokeHelper with an Action set to its
UpdateMessage method.

When a ListItem1 component's InteropCall button is selected, updateMessageCaller is


invoked with a created DotNetObjectReference for the MessageUpdateInvokeHelper
instance. This permits the framework to call UpdateMessageCaller on that ListItem1 's
MessageUpdateInvokeHelper instance. The passed DotNetObjectReference is disposed in

JS ( dotNetHelper.dispose() ).

Shared/ListItem1.razor :

razor

@inject IJSRuntime JS

<li>
@message
<button @onclick="InteropCall"
style="display:@display">InteropCall</button>
</li>

@code {
private string message = "Select one of these list item buttons.";
private string display = "inline-block";
private MessageUpdateInvokeHelper? messageUpdateInvokeHelper;

protected override void OnInitialized()


{
messageUpdateInvokeHelper = new
MessageUpdateInvokeHelper(UpdateMessage);
}

protected async Task InteropCall()


{
if (messageUpdateInvokeHelper is not null)
{
await JS.InvokeVoidAsync("updateMessageCaller",
DotNetObjectReference.Create(messageUpdateInvokeHelper));
}
}

private void UpdateMessage()


{
message = "UpdateMessage Called!";
display = "none";
StateHasChanged();
}
}

StateHasChanged is called to update the UI when message is set in UpdateMessage . If


StateHasChanged isn't called, Blazor has no way of knowing that the UI should be
updated when the Action is invoked.

The following CallDotNetExample6 parent component includes four list items, each an
instance of the ListItem1 component.

Pages/CallDotNetExample6.razor :

razor

@page "/call-dotnet-example-6"

<h1>Call .NET Example 6</h1>

<ul>
<ListItem1 />
<ListItem1 />
<ListItem1 />
<ListItem1 />
</ul>

The following image shows the rendered CallDotNetExample6 parent component after
the second InteropCall button is selected:

The second ListItem1 component has displayed the UpdateMessage Called!


message.
The InteropCall button for the second ListItem1 component isn't visible because
the button's CSS display property is set to none .
Component instance .NET method called from
DotNetObjectReference assigned to an element
property
The assignment of a DotNetObjectReference to a property of an HTML element permits
calling .NET methods on a component instance:

An element reference is captured (ElementReference).


In the component's OnAfterRender{Async} method, a JavaScript (JS) function is
invoked with the element reference and the component instance as a
DotNetObjectReference. The JS function attaches the DotNetObjectReference to
the element in a property.
When an element event is invoked in JS (for example, onclick ), the element's
attached DotNetObjectReference is used to call a .NET method.

Similar to the approach described in the Component instance .NET method helper class
section, this approach is useful in the following scenarios:

When several components of the same type are rendered on the same page.
In Blazor Server apps with multiple users concurrently using the same component.
The .NET method is invoked from a JS event (for example, onclick ), not from a
Blazor event (for example, @onclick ).

In the following example:

The CallDotNetExample7 component contains several ListItem2 components,


which is a shared component in the app's Shared folder.
Each ListItem2 component is composed of a list item message <span> and a
second <span> with a display CSS property set to inline-block for display.
When a ListItem2 component list item is selected, that ListItem2 's UpdateMessage
method changes the list item text in the first <span> and hides the second <span>
by setting its display property to none .

The following assignDotNetHelper JS function assigns the DotNetObjectReference to an


element in a property named dotNetHelper :

HTML

<script>
window.assignDotNetHelper = (element, dotNetHelper) => {
element.dotNetHelper = dotNetHelper;
}
</script>

The following interopCall JS function uses the DotNetObjectReference for the passed
element to invoke a .NET method named UpdateMessage :

HTML

<script>
window.interopCall = async (element) => {
await element.dotNetHelper.invokeMethodAsync('UpdateMessage');
}
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.

The following ListItem2 component is a shared component that can be used any
number of times in a parent component and creates list items ( <li>...</li> ) for an
HTML list ( <ul>...</ul> or <ol>...</ol> ).

Each ListItem2 component instance invokes the assignDotNetHelper JS function in


OnAfterRenderAsync with an element reference (the first <span> element of the list
item) and the component instance as a DotNetObjectReference.
When a ListItem2 component's message <span> is selected, interopCall is invoked
passing the <span> element as a parameter ( this ), which invokes the UpdateMessage
.NET method. In UpdateMessage , StateHasChanged is called to update the UI when
message is set and the display property of the second <span> is updated. If
StateHasChanged isn't called, Blazor has no way of knowing that the UI should be

updated when the method is invoked.

The DotNetObjectReference is disposed when the component is disposed.

Shared/ListItem2.razor :

razor

@inject IJSRuntime JS

<li>
<span @ref="elementRef" onclick="interopCall(this)">@message</span>
<span style="display:@display">Not Updated Yet!</span>
</li>

@code {
private DotNetObjectReference<ListItem2>? objRef;
private ElementReference elementRef;
private string display = "inline-block";
private string message = "Select one of these list items.";

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
objRef = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("assignDotNetHelper", elementRef,
objRef);
}
}

[JSInvokable]
public void UpdateMessage()
{
message = "UpdateMessage Called!";
display = "none";
StateHasChanged();
}

public void Dispose() => objRef?.Dispose();


}

The following CallDotNetExample7 parent component includes four list items, each an
instance of the ListItem2 component.
Pages/CallDotNetExample7.razor :

razor

@page "/call-dotnet-example-7"

<h1>Call .NET Example 7</h1>

<ul>
<ListItem2 />
<ListItem2 />
<ListItem2 />
<ListItem2 />
</ul>

Synchronous JS interop in Blazor WebAssembly


apps
This section only applies to Blazor WebAssembly apps.

JS interop calls are asynchronous by default, regardless of whether the called code is
synchronous or asynchronous. Calls are asynchronous by default to ensure that
components are compatible across both Blazor hosting models, Blazor Server and Blazor
WebAssembly. On Blazor Server, all JS interop calls must be asynchronous because
they're sent over a network connection.

If you know for certain that your app only ever runs on Blazor WebAssembly, you can
choose to make synchronous JS interop calls. This has slightly less overhead than
making asynchronous calls and can result in fewer render cycles because there's no
intermediate state while awaiting results.

To make a synchronous call from JavaScript to .NET in Blazor WebAssembly apps, use
DotNet.invokeMethod instead of DotNet.invokeMethodAsync .

Synchronous calls work if:

The app is running on Blazor WebAssembly, not Blazor Server.


The called function returns a value synchronously. The function isn't an async
method and doesn't return a .NET Task or JavaScript Promise .

JavaScript location
Load JavaScript (JS) code using any of approaches described by the JS interop overview
article:

Load a script in <head> markup (Not generally recommended)


Load a script in <body> markup
Load a script from an external JavaScript file (.js) collocated with a component
Load a script from an external JavaScript file (.js)
Inject a script before or after Blazor starts

Using JS modules to load JS is described in this article in the JavaScript isolation in


JavaScript modules section.

2 Warning

Don't place a <script> tag in a component file ( .razor ) because the <script> tag
can't be updated dynamically.

JavaScript isolation in JavaScript modules


Blazor enables JavaScript (JS) isolation in standard JavaScript modules (ECMAScript
specification ). JavaScript module loading works the same way in Blazor as it does for
other types of web apps, and you're free to customize how modules are defined in your
app. For a guide on how to use JavaScript modules, see MDN Web Docs: JavaScript
modules .

JS isolation provides the following benefits:

Imported JS no longer pollutes the global namespace.


Consumers of a library and components aren't required to import the related JS.

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

Dynamic import with the import() operator is supported with ASP.NET Core and
Blazor:

JavaScript

if ({CONDITION}) import("/additionalModule.js");

In the preceding example, the {CONDITION} placeholder represents a conditional check


to determine if the module should be loaded.
For browser compatibility, see Can I use: JavaScript modules: dynamic import .

Avoid circular object references


Objects that contain circular references can't be serialized on the client for either:

.NET method calls.


JavaScript method calls from C# when the return type has circular references.

Byte array support


Blazor supports optimized byte array JavaScript (JS) interop that avoids
encoding/decoding byte arrays into Base64. The following example uses JS interop to
pass a byte array to .NET.

Provide a sendByteArray JS function. The function is called statically, which includes the
assembly name parameter in the invokeMethodAsync call, by a button in the component
and doesn't return a value:

HTML

<script>
window.sendByteArray = () => {
const data = new Uint8Array([0x45,0x76,0x65,0x72,0x79,0x74,0x68,0x69,
0x6e,0x67,0x27,0x73,0x20,0x73,0x68,0x69,0x6e,0x79,0x2c,
0x20,0x43,0x61,0x70,0x74,0x61,0x69,0x6e,0x2e,0x20,0x4e,
0x6f,0x74,0x20,0x74,0x6f,0x20,0x66,0x72,0x65,0x74,0x2e]);
DotNet.invokeMethodAsync('BlazorSample', 'ReceiveByteArray', data)
.then(str => {
alert(str);
});
};
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

Pages/CallDotNetExample8.razor :

razor
@page "/call-dotnet-example-8"
@using System.Text

<h1>Call .NET Example 8</h1>

<p>
<button onclick="sendByteArray()">Send Bytes</button>
</p>

<p>
Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
<a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
<a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
[JSInvokable]
public static Task<string> ReceiveByteArray(byte[] receivedBytes)
{
return Task.FromResult(
Encoding.UTF8.GetString(receivedBytes, 0,
receivedBytes.Length));
}
}

For information on using a byte array when calling JavaScript from .NET, see Call
JavaScript functions from .NET methods in ASP.NET Core Blazor.

Stream from JavaScript to .NET


Blazor supports streaming data directly from JavaScript to .NET. Streams are requested
using the Microsoft.JSInterop.IJSStreamReference interface.

Microsoft.JSInterop.IJSStreamReference.OpenReadStreamAsync returns a Stream and

uses the following parameters:

maxAllowedSize : Maximum number of bytes permitted for the read operation from
JavaScript, which defaults to 512,000 bytes if not specified.
cancellationToken : A CancellationToken for cancelling the read.

In JavaScript:

JavaScript

function streamToDotNet() {
return new Uint8Array(10000000);
}
In C# code:

C#

var dataReference =
await JS.InvokeAsync<IJSStreamReference>("streamToDotNet");
using var dataReferenceStream =
await dataReference.OpenReadStreamAsync(maxAllowedSize: 10_000_000);

var outputPath = Path.Combine(Path.GetTempPath(), "file.txt");


using var outputFileStream = File.OpenWrite(outputPath);
await dataReferenceStream.CopyToAsync(outputFileStream);

In the preceding example:

JS is an injected IJSRuntime instance. IJSRuntime is registered by the Blazor

framework.
The dataReferenceStream is written to disk ( file.txt ) at the current user's
temporary folder path (GetTempPath).

Call JavaScript functions from .NET methods in ASP.NET Core Blazor covers the reverse
operation, streaming from .NET to JavaScript using a DotNetStreamReference.

ASP.NET Core Blazor file uploads covers how to upload a file in Blazor. For a forms
example that streams <textarea> data in a Blazor Server app, see ASP.NET Core Blazor
forms and input components.

JavaScript [JSImport] / [JSExport] interop


This section applies to Blazor WebAssembly apps.

As an alternative to interacting with JavaScript (JS) in Blazor WebAssembly apps using


Blazor's JS interop mechanism based on the IJSRuntime interface, a JS
[JSImport] / [JSExport] interop API is available to apps targeting .NET 7 or later.

For more information, see JavaScript JSImport/JSExport interop with ASP.NET Core
Blazor WebAssembly.

Disposal of JavaScript interop object references


Examples throughout the JavaScript (JS) interop articles demonstrate typical object
disposal patterns:
When calling .NET from JS, as described in this article, dispose of a created
DotNetObjectReference either from .NET or from JS to avoid leaking .NET memory.

When calling JS from .NET, as described in Call JavaScript functions from .NET
methods in ASP.NET Core Blazor, dispose any created
IJSObjectReference/IJSInProcessObjectReference/ JSObjectReference either from
.NET or from JS to avoid leaking JS memory.

JS interop object references are implemented as a map keyed by an identifier on the


side of the JS interop call that creates the reference. When object disposal is initiated
from either the .NET or JS side, Blazor removes the entry from the map, and the object
can be garbage collected as long as no other strong reference to the object is present.

At a minimum, always dispose objects created on the .NET side to avoid leaking .NET
managed memory.

Document Object Model (DOM) cleanup tasks


during component disposal
Don't execute JS interop code for DOM cleanup tasks during component disposal.
Instead, use the MutationObserver pattern in JavaScript on the client for the following
reasons:

The component may have been removed from the DOM by the time your cleanup
code executes in Dispose{Async} .
In a Blazor Server app, the Blazor renderer may have been disposed by the
framework by the time your cleanup code executes in Dispose{Async} .

The MutationObserver pattern allows you to run a function when an element is


removed from the DOM.

JavaScript interop calls without a circuit


This section only applies to Blazor Server apps.

JavaScript (JS) interop calls can't be issued after a SignalR circuit is disconnected.
Without a circuit during component disposal or at any other time that a circuit doesn't
exist, the following method calls fail and log a message that the circuit is disconnected
as a JSDisconnectedException:

JS interop method calls


IJSRuntime.InvokeAsync
JSRuntimeExtensions.InvokeAsync
JSRuntimeExtensions.InvokeVoidAsync)
Dispose / DisposeAsync calls on any IJSObjectReference.

In order to avoid logging JSDisconnectedException or to log custom information, catch


the exception in a try-catch statement.

For the following component disposal example:

The component implements IAsyncDisposable.


objInstance is an IJSObjectReference.

JSDisconnectedException is caught and not logged.


Optionally, you can log custom information in the catch statement at whatever
log level you prefer. The following example doesn't log custom information
because it assumes the developer doesn't care about when or where circuits are
disconnected during component disposal.

C#

async ValueTask IAsyncDisposable.DisposeAsync()


{
try
{
if (objInstance is not null)
{
await objInstance.DisposeAsync();
}
}
catch (JSDisconnectedException)
{
}
}

If you must clean up your own JS objects or execute other JS code on the client after a
circuit is lost, use the MutationObserver pattern in JS on the client. The
MutationObserver pattern allows you to run a function when an element is removed
from the DOM.

For more information, see the following articles:

Handle errors in ASP.NET Core Blazor apps: The JavaScript interop section discusses
error handling in JS interop scenarios.
ASP.NET Core Razor component lifecycle: The Component disposal with
IDisposable and IAsyncDisposable section describes how to implement disposal

patterns in Razor components.


Additional resources
Call JavaScript functions from .NET methods in ASP.NET Core Blazor
InteropComponent.razor example (dotnet/AspNetCore GitHub repository main
branch) : The main branch represents the product unit's current development for
the next release of ASP.NET Core. To select the branch for a different release (for
example, release/5.0 ), use the Switch branches or tags dropdown list to select
the branch.
Interaction with the Document Object Model (DOM)
Blazor samples GitHub repository (dotnet/blazor-samples)
Handle errors in ASP.NET Core Blazor apps (JavaScript interop section)
Blazor Server threat mitigation: .NET methods invoked from the browser
JavaScript [JSImport] / [JSExport]
interop with ASP.NET Core Blazor
Article • 04/27/2023

This article explains how to interact with JavaScript (JS) in Blazor WebAssembly apps
using JavaScript (JS) [JSImport] / [JSExport] interop API released for apps that adopt
.NET 7 or later.

Blazor provides its own JS interop mechanism based on the IJSRuntime interface, which
is uniformly supported across Blazor hosting models and described in the following
articles:

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


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

IJSRuntime enables library authors to build JS interop libraries that can be shared across
the Blazor ecosystem and remains the recommended approach for JS interop in Blazor.

This article describes an alternative JS interop approach specific to WebAssembly-based


apps available for the first time with the release of .NET 7. These approaches are
appropriate when you only expect to run on client-side WebAssembly and not in the
other Blazor hosting models. Library authors can use these approaches to optimize JS
interop by checking at runtime if the app is running on WebAssembly in a browser
(OperatingSystem.IsBrowser). The approaches described in this article should be used to
replace the obsolete unmarshalled JS interop API when migrating to .NET 7 or later.

7 Note

This article focuses on JS interop in Blazor WebAssembly apps. For guidance on


calling .NET in JavaScript apps, see Run .NET from JavaScript.

Obsolete JavaScript interop API


Unmarshalled JS interop using IJSUnmarshalledRuntime API is obsolete in ASP.NET Core
7.0 or later. Follow the guidance in this article to replace the obsolete API.

Prerequisites
Download and install .NET 7.0 or later if it isn't already installed on the system or if the
system doesn't have the latest version installed.

Namespace
The JS interop API described in this article is controlled by attributes in the
System.Runtime.InteropServices.JavaScript namespace.

Enable unsafe blocks


Enable the AllowUnsafeBlocks property in app's project file, which permits the code
generator in the Roslyn compiler to use pointers for JS interop:

XML

<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

2 Warning

The JS interop API requires enabling AllowUnsafeBlocks. Be careful when


implementing your own unsafe code in .NET apps, which can introduce security and
stability risks. For more information, see Unsafe code, pointer types, and function
pointers.

Call JavaScript from .NET


This section explains how to call JS functions from .NET.

In the following CallJavaScript1 component:

The CallJavaScript1 module is imported asynchronously from the collocated JS


file with JSHost.ImportAsync.
The imported getMessage JS function is called by GetWelcomeMessage .
The returned welcome message string is displayed in the UI via the message field.

Pages/CallJavaScript1.razor :

razor
@page "/call-javascript-1"
@using System.Runtime.InteropServices.JavaScript

<h1>
JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop
(Call JS Example 1)
</h1>

@(message is not null ? message : string.Empty)

@code {
private string? message;

protected override async Task OnInitializedAsync()


{
await JSHost.ImportAsync("CallJavaScript1",
"../Pages/CallJavaScript1.razor.js");

message = GetWelcomeMessage();
}
}

7 Note

Code can include a conditional check for OperatingSystem.IsBrowser to ensure


that the JS interop is only called in Blazor WebAssembly apps running on the client
in a browser. This is important for libraries/NuGet packages that target Blazor
hosting models that aren't based on WebAssembly, such as Blazor Server and
Blazor Hybrid, which can't execute the code provided by this JS interop API.

To import a JS function to call it from C#, use the [JSImport] attribute on a C# method
signature that matches the JS function's signature. The first parameter to the [JSImport]
attribute is the name of the JS function to import, and the second parameter is the
name of the JS module.

In the following example, getMessage is a JS function that returns a string for a module
named CallJavaScript1 . The C# method signature matches: No parameters are passed
to the JS function, and the JS function returns a string . The JS function is called by
GetWelcomeMessage in C# code.

Pages/CallJavaScript1.razor.cs :

C#

using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
namespace BlazorSample.Pages;

[SupportedOSPlatform("browser")]
public partial class CallJavaScript1
{
[JSImport("getMessage", "CallJavaScript1")]
internal static partial string GetWelcomeMessage();
}

The app's namespace for the preceding CallJavaScript1 partial class is BlazorSample .
The component's namespace is BlazorSample.Pages . If using the preceding component
in a local test app, update the namespace to match the app. For example, the
namespace is ContosoApp.Pages if the app's namespace is ContosoApp . For more
information, see ASP.NET Core Razor components.

In the imported method signature, you can use .NET types for parameters and return
values, which are marshalled automatically by the runtime. Use
JSMarshalAsAttribute<T> to control how the imported method parameters are
marshalled. For example, you might choose to marshal a long as
System.Runtime.InteropServices.JavaScript.JSType.Number or
System.Runtime.InteropServices.JavaScript.JSType.BigInt. You can pass
Action/Func<TResult> callbacks as parameters, which are marshalled as callable JS
functions. You can pass both JS and managed object references, and they are marshaled
as proxy objects, keeping the object alive across the boundary until the proxy is garbage
collected. You can also import and export asynchronous methods with a Task result,
which are marshaled as JS promises . Most of the marshalled types work in both
directions, as parameters and as return values, on both imported and exported
methods, which are covered in the Call .NET from JavaScript section later in this article.

The following table indicates the supported type mappings.

.NET JavaScript Nullable Task ➔ JSMarshalAs Array


Promise optional of

Boolean Boolean ✅ ✅ ✅

Byte Number ✅ ✅ ✅ ✅

Char String ✅ ✅ ✅

Int16 Number ✅ ✅ ✅

Int32 Number ✅ ✅ ✅ ✅

Int64 Number ✅ ✅
.NET JavaScript Nullable Task ➔ JSMarshalAs Array
Promise optional of

Int64 BigInt ✅ ✅

Single Number ✅ ✅ ✅

Double Number ✅ ✅ ✅ ✅

IntPtr Number ✅ ✅ ✅

DateTime Date ✅ ✅

DateTimeOffset Date ✅ ✅

Exception Error ✅ ✅

JSObject Object ✅ ✅ ✅

String String ✅ ✅ ✅

Object Any ✅ ✅

Span<Byte> MemoryView

Span<Int32> MemoryView

Span<Double> MemoryView

ArraySegment<Byte> MemoryView

ArraySegment<Int32> MemoryView

ArraySegment<Double> MemoryView

Task Promise ✅

Action Function

Action<T1> Function

Action<T1, T2> Function

Action<T1, T2, T3> Function

Func<TResult> Function

Func<T1, TResult> Function

Func<T1, T2, TResult> Function


.NET JavaScript Nullable Task ➔ JSMarshalAs Array
Promise optional of

Func<T1, T2, T3, Function


TResult>

The following conditions apply to type mapping and marshalled values:

The Array of column indicates if the .NET type can be marshalled as a JS Array .
Example: C# int[] ( Int32 ) mapped to JS Array of Number s.
When passing a JS value to C# with a value of the wrong type, the framework
throws an exception in most cases. The framework doesn't perform compile-time
type checking in JS.
JSObject , Exception , Task and ArraySegment create GCHandle and a proxy. You
can trigger disposal in developer code or allow .NET garbage collection (GC) to
dispose of the objects later. These types carry significant performance overhead.
Array : Marshaling an array creates a copy of the array in JS or .NET.

MemoryView

MemoryView is a JS class for the .NET WebAssembly runtime to marshal Span and
ArraySegment .

Unlike marshaling an array, marshaling a Span or ArraySegment doesn't create a


copy of the underlying memory.
MemoryView can only be properly instantiated by the .NET WebAssembly

runtime. Therefore, it isn't possible to import a JS function as a .NET method


that has a parameter of Span or ArraySegment .
MemoryView created for a Span is only valid for the duration of the interop call.
As Span is allocated on the call stack, which doesn't persist after the interop call,
it isn't possible to export a .NET method that returns a Span .
MemoryView created for an ArraySegment survives after the interop call and is
useful for sharing a buffer. Calling dispose() on a MemoryView created for an
ArraySegment disposes the proxy and unpins the underlying .NET array. We
recommend calling dispose() in a try-finally block for MemoryView .

The module name in the [JSImport] attribute and the call to load the module in the
component with JSHost.ImportAsync must match and be unique in the app. When
authoring a library for deployment in a NuGet package, we recommend using the NuGet
package namespace as a prefix in module names. In the following example, the module
name reflects the Contoso.InteropServices.JavaScript package and a folder of user
message interop classes ( UserMessages ):
C#

[JSImport("getMessage",
"Contoso.InteropServices.JavaScript.UserMessages.CallJavaScript1")]

Functions accessible on the global namespace can be imported by using the


globalThis prefix in the function name and by using the [JSImport] attribute without
providing a module name. In the following example, console.log is prefixed with
globalThis . The imported function is called by the C# Log method, which accepts a C#

string message ( message ) and marshalls the C# string to a JS String for console.log :

C#

[JSImport("globalThis.console.log")]
internal static partial void Log([JSMarshalAs<JSType.String>] string
message);

Export scripts from a standard JavaScript ES6 module either collocated with a
component or placed with other JavaScript static assets in a JS file (for example,
wwwroot/js/{FILE NAME}.js , where JS static assets are maintained in a folder named js
in the app's wwwroot folder and the {FILE NAME} placeholder is the file name).

In the following example, a JS function named getMessage is exported from a collocated


JS file that returns a welcome message, "Hello from Blazor!" in Portuguese:

Pages/CallJavaScript1.razor.js :

JavaScript

export function getMessage() {


return 'Olá do Blazor!';
}

Call .NET from JavaScript


This section explains how to call .NET methods from JS.

The following CallDotNet1 component calls JS that directly interacts with the DOM to
render the welcome message string:

The CallDotNet JS module is imported asynchronously from the collocated JS file


for this component.
The imported setMessage JS function is called by SetWelcomeMessage .
The returned welcome message is displayed by setMessage in the UI via the
message field.

) Important

In this section's example, JS interop is used to mutate a DOM element purely for
demonstration purposes after the component is rendered in OnAfterRender.
Typically, you should only mutate the DOM with JS when the object doesn't interact
with Blazor. The approach shown in this section is similar to cases where a third-
party JS library is used in a Razor component, where the component interacts with
the JS library via JS interop, the third-party JS library interacts with part of the DOM,
and Blazor isn't involved directly with the DOM updates to that part of the DOM.
For more information, see ASP.NET Core Blazor JavaScript interoperability (JS
interop).

Pages/CallDotNet1.razor :

razor

@page "/call-dotnet-1"
@using System.Runtime.InteropServices.JavaScript

<h1>
JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop
(Call .NET Example 1)
</h1>

<p>
<span id="result">.NET method not executed yet</span>
</p>

@code {
protected override async Task OnInitializedAsync()
{
await JSHost.ImportAsync("CallDotNet1",
"../Pages/CallDotNet1.razor.js");
}

protected override void OnAfterRender(bool firstRender)


{
SetWelcomeMessage();
}
}

To export a .NET method so that it can be called from JS, use the [JSExport] attribute.

In the following example:


SetWelcomeMessage calls a JS function named setMessage . The JS function calls into

.NET to receive the welcome message from GetMessageFromDotnet and displays the
message in the UI.
GetMessageFromDotnet is a .NET method with the [JSExport] attribute that returns
a welcome message, "Hello from Blazor!" in Portuguese.

Pages/CallDotNet1.razor.cs :

C#

using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

namespace BlazorSample.Pages;

[SupportedOSPlatform("browser")]
public partial class CallDotNet1
{
[JSImport("setMessage", "CallDotNet1")]
internal static partial void SetWelcomeMessage();

[JSExport]
internal static string GetMessageFromDotnet()
{
return "Olá do Blazor!";
}
}

The app's namespace for the preceding CallDotNet1 partial class is BlazorSample . The
component's namespace is BlazorSample.Pages . If using the preceding component in a
local test app, update the app's namespace to match the app. For example, the
component namespace is ContosoApp.Pages if the app's namespace is ContosoApp . For
more information, see ASP.NET Core Razor components.

In the following example, a JS function named setMessage is imported from a collocated


JS file.

The setMessage method:

Calls globalThis.getDotnetRuntime(0) to expose the WebAssembly .NET runtime


instance for calling exported .NET methods.
Obtains the app assembly's JS exports. The name of the app's assembly in the
following example is BlazorSample .
Calls the BlazorSample.Pages.CallDotNet1.GetMessageFromDotnet method from the
exports ( exports ). The returned value, which is the welcome message, is assigned
to the CallDotNet1 component's <span> text. The app's namespace is
BlazorSample , and the CallDotNet1 component's namespace is
BlazorSample.Pages .

Pages/CallDotNet1.razor.js :

JavaScript

export async function setMessage() {


const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
var exports = await getAssemblyExports("BlazorSample.dll");

document.getElementById("result").innerText =
exports.BlazorSample.Pages.CallDotNet1.GetMessageFromDotnet();
}

7 Note

Calling getAssemblyExports to obtain the exports can occur in a JavaScript


initializer for availability across the app.

Multiple module import calls


After a JS module is loaded, the module's JS functions are available to the app's
components and classes as long as the app is running in the browser window or tab
without the user manually reloading the app. JSHost.ImportAsync can be called multiple
times on the same module without a significant performance penalty when:

The user visits a component that calls JSHost.ImportAsync to import a module,


navigates away from the component, and then returns to the component where
JSHost.ImportAsync is called again for the same module import.
The same module is used by different components and loaded by
JSHost.ImportAsync in each of the components.

Use of a single JavaScript module across


components
Before following the guidance in this section, read the Call JavaScript from .NET and Call
.NET from JavaScript sections of this article, which provide general guidance on
[JSImport] / [JSExport] interop.
The example in this section shows how to use JS interop from a shared JS module in a
Blazor WebAssembly app. The guidance in this section isn't applicable to Razor class
libraries (RCLs).

The following components, classes, C# methods, and JS functions are used:

Interop class ( Interop.cs ): Sets up import and export JS interop with the

[JSImport] and [JSExport] attributes for a module named Interop .

GetWelcomeMessage : .NET method that calls the imported getMessage JS function.


SetWelcomeMessage : .NET method that calls the imported setMessage JS function.

GetMessageFromDotnet : An exported C# method that returns a welcome message


string when called from JS.
wwwroot/js/interop.js file: Contains the JS functions.

getMessage : Returns a welcome message when called by C# code in a


component.
setMessage : Calls the GetMessageFromDotnet C# method and assigns the
returned welcome message to a DOM <span> element.
Program.cs calls JSHost.ImportAsync to load the module from

wwwroot/js/interop.js .
CallJavaScript2 component ( Pages/CallJavaScript2.razor ): Calls

GetWelcomeMessage and displays the returned welcome message in the


component's UI.
CallDotNet2 component ( Pages/CallDotNet2.razor ): Calls SetWelcomeMessage .

Interop.cs :

C#

using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

namespace BlazorSample.JavaScriptInterop;

[SupportedOSPlatform("browser")]
public partial class Interop
{
[JSImport("getMessage", "Interop")]
internal static partial string GetWelcomeMessage();

[JSImport("setMessage", "Interop")]
internal static partial void SetWelcomeMessage();

[JSExport]
internal static string GetMessageFromDotnet()
{
return "Olá do Blazor!";
}
}

In the preceding example, the app's namespace is BlazorSample , and the full namespace
for C# interop classes is BlazorSample.JavaScriptInterop .

wwwroot/js/interop.js :

JavaScript

export function getMessage() {


return 'Olá do Blazor!';
}

export async function setMessage() {


const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
var exports = await getAssemblyExports("BlazorSample.dll");

document.getElementById("result").innerText =
exports.BlazorSample.JavaScriptInterop.Interop.GetMessageFromDotnet();
}

Make the System.Runtime.InteropServices.JavaScript namespace available at the top of


the Program.cs file:

C#

using System.Runtime.InteropServices.JavaScript;

Load the module in Program.cs before WebAssemblyHost.RunAsync is called:

C#

await JSHost.ImportAsync("Interop", "../js/interop.js");

Pages/CallJavaScript2.razor :

razor

@page "/call-javascript-2"
@using BlazorSample.JavaScriptInterop

<h1>
JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop
(Call JS Example 2)
</h1>
@(message is not null ? message : string.Empty)

@code {
private string? message;

protected override void OnInitializedAsync()


{
message = Interop.GetWelcomeMessage();
}
}

Pages/CallDotNet2.razor :

razor

@page "/call-dotnet-2"
@using BlazorSample.JavaScriptInterop

<h1>
JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop
(Call .NET Example 2)
</h1>

<p>
<span id="result">.NET method not executed</span>
</p>

@code {
protected override void OnAfterRender(bool firstRender)
{
Interop.SetWelcomeMessage();
}
}

) Important

In this section's example, JS interop is used to mutate a DOM element purely for
demonstration purposes after the component is rendered in OnAfterRender.
Typically, you should only mutate the DOM with JS when the object doesn't interact
with Blazor. The approach shown in this section is similar to cases where a third-
party JS library is used in a Razor component, where the component interacts with
the JS library via JS interop, the third-party JS library interacts with part of the DOM,
and Blazor isn't involved directly with the DOM updates to that part of the DOM.
For more information, see ASP.NET Core Blazor JavaScript interoperability (JS
interop).
Additional resources
API documentation
[JSImport] attribute
[JSExport] attribute
Run .NET from JavaScript
In the dotnet/runtime GitHub repository:
.NET WebAssembly runtime
dotnet.d.ts file (.NET WebAssembly runtime configuration)
Call a web API from ASP.NET Core
Blazor
Article • 04/06/2023

This article describes how to call a web API from a Blazor app.

7 Note

The code examples in this article adopt nullable reference types (NRTs) and .NET
compiler null-state static analysis, which are supported in ASP.NET Core 6.0 or
later. When targeting ASP.NET Core 5.0 or earlier, remove the null type designation
( ? ) from the string? , TodoItem[]? , WeatherForecast[]? , and
IEnumerable<GitHubBranch>? types in the article's examples.

7 Note

This article has loaded Blazor Server coverage for calling web APIs. The Blazor
WebAssembly coverage addresses the following subjects:

Blazor WebAssembly examples based on an client-side WebAssembly app


that calls a web API to create, read, update, and delete todo list items.
System.Net.Http.Json package.

HttpClient service configuration.

HttpClient and JSON helpers ( GetFromJsonAsync , PostAsJsonAsync ,

PutAsJsonAsync , DeleteAsync ).

IHttpClientFactory services and the configuration of a named HttpClient .

Typed HttpClient .
HttpClient and HttpRequestMessage to customize requests.

Call web API example with cross-origin resource sharing (CORS) and how
CORS pertains to Blazor WebAssembly apps.
How to handle web API response errors in developer code.
Blazor framework component examples for testing web API access.
Additional resources for developing Blazor WebAssembly apps that call a web
API.
Blazor Server apps call web APIs using HttpClient instances, typically created using
IHttpClientFactory. For guidance that applies to Blazor Server, see Make HTTP requests
using IHttpClientFactory in ASP.NET Core.

A Blazor Server app doesn't include an HttpClient service by default. Provide an


HttpClient to the app using the HttpClient factory infrastructure.

In Program.cs :

C#

builder.Services.AddHttpClient();

The following Blazor Server Razor component makes a request to a web API for GitHub
branches similar to the Basic Usage example in the Make HTTP requests using
IHttpClientFactory in ASP.NET Core article.

Pages/CallWebAPI.razor :

razor

@page "/call-web-api"
@using System.Text.Json
@using System.Text.Json.Serialization
@inject IHttpClientFactory ClientFactory

<h1>Call web API from a Blazor Server Razor component</h1>

@if (getBranchesError || branches is null)


{
<p>Unable to get branches from GitHub. Please try again later.</p>
}
else
{
<ul>
@foreach (var branch in branches)
{
<li>@branch.Name</li>
}
</ul>
}

@code {
private IEnumerable<GitHubBranch>? branches = Array.Empty<GitHubBranch>
();
private bool getBranchesError;
private bool shouldRender;

protected override bool ShouldRender() => shouldRender;


protected override async Task OnInitializedAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
request.Headers.Add("Accept", "application/vnd.github.v3+json");
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

var client = ClientFactory.CreateClient();

var response = await client.SendAsync(request);

if (response.IsSuccessStatusCode)
{
using var responseStream = await
response.Content.ReadAsStreamAsync();
branches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(responseStream);
}
else
{
getBranchesError = true;
}

shouldRender = true;
}

public class GitHubBranch


{
[JsonPropertyName("name")]
public string? Name { get; set; }
}
}

For an additional working example, see the Blazor Server file upload example that
uploads files to a web API controller in the ASP.NET Core Blazor file uploads article.

Cross-origin resource sharing (CORS)


Browser security restricts a webpage from making requests to a different domain than
the one that served the webpage. This restriction is called the same-origin policy. The
same-origin policy restricts (but doesn't prevent) a malicious site from reading sensitive
data from another site. To make requests from the browser to an endpoint with a
different origin, the endpoint must enable cross-origin resource sharing (CORS) .

For more information, see Enable Cross-Origin Requests (CORS) in ASP.NET Core.
Blazor framework component examples for
testing web API access
Various network tools are publicly available for testing web API backend apps directly,
such as Firefox Browser Developer and Postman . Blazor framework's reference
source includes HttpClient test assets that are useful for testing:

HttpClientTest assets in the dotnet/aspnetcore GitHub repository

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Additional resources
ASP.NET Core Blazor Server additional security scenarios: Includes coverage on
using HttpClient to make secure web API requests.
Make HTTP requests using IHttpClientFactory in ASP.NET Core
Enforce HTTPS in ASP.NET Core
Enable Cross-Origin Requests (CORS) in ASP.NET Core
Kestrel HTTPS endpoint configuration
Cross-Origin resource sharing (CORS) at W3C

:::moniker-end
Work with images in ASP.NET Core
Blazor
Article • 04/04/2023

This article describes common scenarios for working with images in Blazor apps.

Dynamically set an image source


The following example demonstrates how to dynamically set an image's source with a
C# field.

For the example in this section:

Obtain three images from any source or right-click each of the following images to
save them locally. Name the images image1.png , image2.png , and image3.png .

Place the images in a new folder named images in the app's web root ( wwwroot ).
The use of the images folder is only for demonstration purposes. You can organize
images in any folder layout that you prefer, including serving the images directly
from the wwwroot folder.

In the following ShowImage1 component:

The image's source ( src ) is dynamically set to the value of imageSource in C#.
The ShowImage method updates the imageSource field based on an image id
argument passed to the method.
Rendered buttons call the ShowImage method with an image argument for each of
the three available images in the images folder. The file name is composed using
the argument passed to the method and matches one of the three images in the
images folder.

Pages/ShowImage1.razor :

razor

@page "/show-image-1"

<h1>Dynamic Image Source Example</h1>


@if (imageSource is not null)
{
<p>
<img src="@imageSource" />
</p>
}

@for (var i = 1; i <= 3; i++)


{
var imageId = i;
<button @onclick="() => ShowImage(imageId)">
Image @imageId
</button>
}

@code {
private string? imageSource;

private void ShowImage(int id)


{
imageSource = $"images/image{id}.png";
}
}

The preceding example uses a C# field to hold the image's source data, but you can also
use a C# property to hold the data.

7 Note

Avoid using a loop variable directly in a lambda expression, such as i in the


preceding for loop example. Otherwise, the same variable is used by all lambda
expressions, which results in use of the same value in all lambdas. Capture the
variable's value in a local variable. In the preceding example:

The loop variable i is assigned to imageId .


imageId is used in the lambda expression.

Alternatively, use a foreach loop with Enumerable.Range, which doesn't suffer


from the preceding problem:

razor

@foreach (var imageId in Enumerable.Range(1,3))


{
<button @onclick="() => ShowImage(imageId)">
Image @imageId
</button>
}
For more information, see ASP.NET Core Blazor event handling.

Stream image data


An image can be directly sent to the client using Blazor's streaming interop features
instead of hosting the image at a public URL.

The example in this section streams image source data using JavaScript (JS) interop. The
following setImage JS function accepts the <img> tag id and data stream for the image.
The function performs the following steps:

Reads the provided stream into an ArrayBuffer .


Creates a Blob to wrap the ArrayBuffer .
Creates an object URL to serve as the address for the image to be shown.
Updates the <img> element with the specified imageElementId with the object URL
just created.
To prevent memory leaks, the function calls revokeObjectURL to dispose of the
object URL when the component is finished working with an image.

HTML

<script>
window.setImage = async (imageElementId, imageStream) => {
const arrayBuffer = await imageStream.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
const image = document.getElementById(imageElementId);
image.onload = () => {
URL.revokeObjectURL(url);
}
image.src = url;
}
</script>

7 Note

For general guidance on JS location and our recommendations for production


apps, see ASP.NET Core Blazor JavaScript interoperability (JS interop).

The following ShowImage2 component:


Injects services for an System.Net.Http.HttpClient and
Microsoft.JSInterop.IJSRuntime.
Includes an <img> tag to display an image.
Has a GetImageStreamAsync C# method to retrieve a Stream for an image. A
production app may dynamically generate an image based on the specific user or
retrieve an image from storage. The following example retrieves the .NET avatar for
the dotnet GitHub repository.
Has a SetImageAsync method that's triggered on the button's selection by the user.
SetImageAsync performs the following steps:
Retrieves the Stream from GetImageStreamAsync .
Wraps the Stream in a DotNetStreamReference, which allows streaming the
image data to the client.
Invokes the setImage JavaScript function, which accepts the data on the client.

7 Note

Blazor Server apps use a dedicated HttpClient service to make requests, so no


action is required by the developer in Blazor Server apps to register an HttpClient
service. Blazor WebAssembly apps have a default HttpClient service registration
when the app is created from a Blazor WebAssembly project template. If an
HttpClient service registration isn't present in Program.cs of a Blazor WebAssembly
app, provide one by adding builder.Services.AddHttpClient(); . For more
information, see Make HTTP requests using IHttpClientFactory in ASP.NET Core.

Pages/ShowImage2.razor :

razor

@page "/show-image-2"
@inject HttpClient Http
@inject IJSRuntime JS

<h1>Stream Image Data Example</h1>

<p>
<img id="image" />
</p>

<button @onclick="SetImageAsync">
Set Image
</button>

@code {
private async Task<Stream> GetImageStreamAsync()
{
return await Http.GetStreamAsync(
"https://avatars.githubusercontent.com/u/9141961");
}

private async Task SetImageAsync()


{
var imageStream = await GetImageStreamAsync();
var dotnetImageStream = new DotNetStreamReference(imageStream);
await JS.InvokeVoidAsync("setImage", "image", dotnetImageStream);
}
}

Additional resources
ASP.NET Core Blazor file uploads
ASP.NET Core Blazor file downloads
Call .NET methods from JavaScript functions in ASP.NET Core Blazor
Call JavaScript functions from .NET methods in ASP.NET Core Blazor
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor authentication and
authorization
Article • 03/29/2023

This article describes ASP.NET Core's support for the configuration and management of
security in Blazor apps.

Security scenarios differ between Blazor Server and Blazor WebAssembly apps. Because
Blazor Server apps run on the server, authorization checks are able to determine:

The UI options presented to a user (for example, which menu entries are available
to a user).
Access rules for areas of the app and components.

Blazor WebAssembly apps run on the client. Authorization is only used to determine
which UI options to show. Since client-side checks can be modified or bypassed by a
user, a Blazor WebAssembly app can't enforce authorization access rules.

Razor Pages authorization conventions don't apply to routable Razor components. If a


non-routable Razor component is embedded in a page of a Razor Pages app, the page's
authorization conventions indirectly affect the Razor component along with the rest of
the page's content.

ASP.NET Core Identity is designed to work in the context of HTTP request and response
communication, which generally isn't the Blazor app client-server communication
model. ASP.NET Core apps that use ASP.NET Core Identity for user management should
use Razor Pages instead of Razor components for Identity-related UI, such as user
registration, login, logout, and other user management tasks. Building Razor
components that directly handle Identity tasks is possible for several scenarios but isn't
recommended or supported by Microsoft.

ASP.NET Core abstractions, such as SignInManager<TUser> and UserManager<TUser>,


aren't supported in Razor components. For more information on using ASP.NET Core
Identity with Blazor, see Scaffold ASP.NET Core Identity into a Blazor Server app.

7 Note

The code examples in this article adopt nullable reference types (NRTs) and .NET
compiler null-state static analysis, which are supported in ASP.NET Core 6.0 or
later. When targeting ASP.NET Core 5.0 or earlier, remove the null type designation
( ? ) from examples in this article.
Authentication
Blazor uses the existing ASP.NET Core authentication mechanisms to establish the user's
identity. The exact mechanism depends on how the Blazor app is hosted, Blazor Server
or Blazor WebAssembly.

Blazor Server authentication


Blazor Server operates over a SignalR connection with the client. Authentication in
SignalR-based apps is handled when the connection is established. Authentication can
be based on a cookie or some other bearer token, but authentication is managed via the
SignalR hub and entirely within the circuit.

The built-in AuthenticationStateProvider service for Blazor Server apps obtains


authentication state data from ASP.NET Core's HttpContext.User . This is how
authentication state integrates with existing ASP.NET Core authentication mechanisms.

Avoid IHttpContextAccessor / HttpContext in Razor components

Don't use IHttpContextAccessor/HttpContext directly or indirectly in the Razor


components of Blazor Server apps. Blazor apps run outside of the ASP.NET Core
pipeline context. The HttpContext isn't guaranteed to be available within the
IHttpContextAccessor, and HttpContext isn't guaranteed to hold the context that started
the Blazor app.

The recommended approach for passing request state to the Blazor app is through root
component parameters during the app's initial rendering. Alternatively, the app can
copy the data into a scoped service in the root component's initialization lifecycle event
for use across the app. For more information, see ASP.NET Core Blazor Server additional
security scenarios.

A critical aspect of Blazor Server security is that the user attached to a given circuit
might become updated at some point after the Blazor circuit is established but the
IHttpContextAccessor isn't updated. For more information on addressing this situation
with custom services, see ASP.NET Core Blazor Server additional security scenarios.

Shared state

Blazor server apps live in server memory, and multiple app sessions are hosted within
the same process. For each app session, Blazor starts a circuit with its own dependency
injection container scope, thus scoped services are unique per Blazor session.

2 Warning

We don't recommend apps on the same server share state using singleton services
unless extreme care is taken, as this can introduce security vulnerabilities, such as
leaking user state across circuits.

You can use stateful singleton services in Blazor apps if they're specifically designed for
it. For example, use of a singleton memory cache is acceptable because a memory cache
requires a key to access a given entry. Assuming users don't have control over the cache
keys that are used with the cache, state stored in the cache doesn't leak across circuits.

For general guidance on state management, see ASP.NET Core Blazor state
management.

Blazor WebAssembly authentication


In Blazor WebAssembly, authentication checks can be bypassed because all client-side
code can be modified by users. The same is true for all client-side app technologies,
including JavaScript SPA frameworks and native apps for any operating system.

Add the following:

A package reference for the Microsoft.AspNetCore.Components.Authorization


NuGet package.

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 .

The Microsoft.AspNetCore.Components.Authorization namespace to the app's


_Imports.razor file.

To handle authentication, use of the built-in or custom AuthenticationStateProvider


service is covered in the following sections.

For more information, see Secure ASP.NET Core Blazor WebAssembly.


AuthenticationStateProvider service
AuthenticationStateProvider is the underlying service used by the AuthorizeView
component and CascadingAuthenticationState component to obtain the authentication
state for a user.

You don't typically use AuthenticationStateProvider directly. Use the AuthorizeView


component or Task<AuthenticationState> approaches described later in this article. The
main drawback to using AuthenticationStateProvider directly is that the component isn't
notified automatically if the underlying authentication state data changes.

7 Note

To implement a custom AuthenticationStateProvider, see Secure ASP.NET Core


Blazor Server apps.

The AuthenticationStateProvider service can provide the current user's ClaimsPrincipal


data, as shown in the following example.

Pages/ClaimsPrincipalData.razor :

razor

@page "/claims-principle-data"
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>ClaimsPrincipal Data</h1>

<button @onclick="GetClaimsPrincipalData">Get ClaimsPrincipal Data</button>

<p>@authMessage</p>

@if (claims.Count() > 0)


{
<ul>
@foreach (var claim in claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
}

<p>@surname</p>

@code {
private string? authMessage;
private string? surname;
private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

private async Task GetClaimsPrincipalData()


{
var authState = await AuthenticationStateProvider
.GetAuthenticationStateAsync();
var user = authState.User;

if (user.Identity is not null && user.Identity.IsAuthenticated)


{
authMessage = $"{user.Identity.Name} is authenticated.";
claims = user.Claims;
surname = user.FindFirst(c => c.Type ==
ClaimTypes.Surname)?.Value;
}
else
{
authMessage = "The user is NOT authenticated.";
}
}
}

If user.Identity.IsAuthenticated is true and because the user is a ClaimsPrincipal,


claims can be enumerated and membership in roles evaluated.

For more information on dependency injection (DI) and services, see ASP.NET Core
Blazor dependency injection and Dependency injection in ASP.NET Core. For information
on how to implement a custom AuthenticationStateProvider in Blazor Server apps, see
Secure ASP.NET Core Blazor Server apps.

Expose the authentication state as a cascading


parameter
If authentication state data is required for procedural logic, such as when performing an
action triggered by the user, obtain the authentication state data by defining a
cascading parameter of type Task< AuthenticationState > , as the following example
demonstrates.

Pages/CascadeAuthState.razor :

razor

@page "/cascade-auth-state"

<h1>Cascade Auth State</h1>

<p>@authMessage</p>
@code {
private string authMessage = "The user is NOT authenticated.";

[CascadingParameter]
private Task<AuthenticationState>? authenticationState { get; set; }

protected override async Task OnInitializedAsync()


{
if (authenticationState is not null)
{
var authState = await authenticationState;
var user = authState?.User;

if (user?.Identity is not null && user.Identity.IsAuthenticated)


{
authMessage = $"{user.Identity.Name} is authenticated.";
}
}
}
}

If user.Identity.IsAuthenticated is true , claims can be enumerated and membership


in roles evaluated.

Set up the Task< AuthenticationState > cascading parameter using the


AuthorizeRouteView and CascadingAuthenticationState components in the App
component.

7 Note

When you create a Blazor app from one of the Blazor project templates with
authentication enabled, the App component includes the AuthorizeRouteView and
CascadingAuthenticationState components shown in the following example. A
Blazor WebAssembly app includes the required service registrations as well.
Additional information is presented in the Customize unauthorized content with
the Router component section.

App.razor :

razor

<CascadingAuthenticationState>
<Router ...>
<Found ...>
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
...
</Found>
<NotFound>
...
</NotFound>
</Router>
</CascadingAuthenticationState>

In a Blazor WebAssembly App, add services for options and authorization to Program.cs :

C#

builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();

In a Blazor Server app, services for options and authorization are already present, so no
further steps are required.

Authorization
After a user is authenticated, authorization rules are applied to control what the user can
do.

Access is typically granted or denied based on whether:

A user is authenticated (signed in).


A user is in a role.
A user has a claim.
A policy is satisfied.

Each of these concepts is the same as in an ASP.NET Core MVC or Razor Pages app. For
more information on ASP.NET Core security, see the articles under ASP.NET Core
Security and Identity.

AuthorizeView component
The AuthorizeView component selectively displays UI content depending on whether
the user is authorized. This approach is useful when you only need to display data for
the user and don't need to use the user's identity in procedural logic.

The component exposes a context variable of type AuthenticationState ( @context in


Razor syntax), which you can use to access information about the signed-in user:

razor
<AuthorizeView>
<p>Hello, @context.User.Identity?.Name!</p>
</AuthorizeView>

You can also supply different content for display if the user isn't authorized:

razor

<AuthorizeView>
<Authorized>
<p>Hello, @context.User.Identity?.Name!</p>
<p><button @onclick="SecureMethod">Authorized Only Button</button>
</p>
</Authorized>
<NotAuthorized>
<p>You're not authorized.</p>
</NotAuthorized>
</AuthorizeView>

@code {
private void SecureMethod() { ... }
}

A default event handler for an authorized element, such as the SecureMethod method for
the <button> element in the preceding example, can only be invoked by an authorized
user.

2 Warning

Client-side markup and methods associated with an AuthorizeView are only


protected from view and execution in the rendered UI in Blazor WebAssembly apps.
In order to protect authorized content and secure methods for Blazor
WebAssembly apps, the content is usually supplied by a secure, authorized web API
call to a server API and never stored in the app. For more information, see Call a
web API from an ASP.NET Core Blazor app and ASP.NET Core Blazor
WebAssembly additional security scenarios.

The content of <Authorized> and <NotAuthorized> tags can include arbitrary items, such
as other interactive components.

Authorization conditions, such as roles or policies that control UI options or access, are
covered in the Authorization section.

If authorization conditions aren't specified, AuthorizeView uses a default policy:


Authenticated (signed-in) users are authorized.
Unauthenticated (signed-out) users are unauthorized.

The AuthorizeView component can be used in the NavMenu component


( Shared/NavMenu.razor ) to display a NavLink component (NavLink), but note that this
approach only removes the list item from the rendered output. It doesn't prevent the
user from navigating to the component. Implement authorization separately in the
destination component.

Role-based and policy-based authorization


The AuthorizeView component supports role-based or policy-based authorization.

For role-based authorization, use the Roles parameter. In the following example, the
user must have a role claim for either the Admin or Superuser roles:

razor

<AuthorizeView Roles="Admin, Superuser">


<p>You have an 'Admin' or 'Superuser' role claim.</p>
</AuthorizeView>

To require a user have both Admin and Superuser role claims, nest AuthorizeView
components:

razor

<AuthorizeView Roles="Admin">
<p>User: @context.User</p>
<p>You have the 'Admin' role claim.</p>
<AuthorizeView Roles="Superuser" Context="innerContext">
<p>User: @innerContext.User</p>
<p>You have both 'Admin' and 'Superuser' role claims.</p>
</AuthorizeView>
</AuthorizeView>

The preceding code establishes a Context for the inner AuthorizeView component to
prevent an AuthenticationState context collision. The AuthenticationState context is
accessed in the outer AuthorizeView with the standard approach for accessing the
context ( @context.User ). The context is accessed in the inner AuthorizeView with the
named innerContext context ( @innerContext.User ).

For more information, including configuration guidance, see Role-based authorization in


ASP.NET Core.
For policy-based authorization, use the Policy parameter with a single policy:

razor

<AuthorizeView Policy="Over21">
<p>You satisfy the 'Over21' policy.</p>
</AuthorizeView>

To handle the case where the user should satisfy one of several policies, create a policy
that confirms that the user satisfies other policies.

To handle the case where the user must satisfy several policies simultaneously, take
either of the following approaches:

Create a policy for AuthorizeView that confirms that the user satisfies several other
policies.

Nest the policies in multiple AuthorizeView components:

razor

<AuthorizeView Policy="Over21">
<AuthorizeView Policy="LivesInCalifornia">
<p>You satisfy the 'Over21' and 'LivesInCalifornia' policies.
</p>
</AuthorizeView>
</AuthorizeView>

Claims-based authorization is a special case of policy-based authorization. For example,


you can define a policy that requires users to have a certain claim. For more information,
see Policy-based authorization in ASP.NET Core.

If neither Roles nor Policy is specified, AuthorizeView uses the default policy:

Authenticated (signed-in) users are authorized.


Unauthenticated (signed-out) users are unauthorized.

Because .NET string comparisons are case-sensitive by default, matching role and policy
names is also case-sensitive. For example, Admin (uppercase A ) is not treated as the
same role as admin (lowercase a ).

Pascal case is typically used for role and policy names (for example,
BillingAdministrator ), but the use of Pascal case isn't a strict requirement. Different
casing schemes, such as camel case, kebab case, and snake case, are permitted. Using
spaces in role and policy names is unusual but permitted by the framework. For
example, billing administrator is an unusual role or policy name format in .NET apps,
but it's a valid role or policy name.

Content displayed during asynchronous authentication


Blazor allows for authentication state to be determined asynchronously. The primary
scenario for this approach is in Blazor WebAssembly apps that make a request to an
external endpoint for authentication.

While authentication is in progress, AuthorizeView displays no content by default. To


display content while authentication occurs, use the <Authorizing> tag:

razor

<AuthorizeView>
<Authorized>
<p>Hello, @context.User.Identity?.Name!</p>
</Authorized>
<Authorizing>
<p>You can only see this content while authentication is in
progress.</p>
</Authorizing>
</AuthorizeView>

This approach isn't normally applicable to Blazor Server apps. Blazor Server apps know
the authentication state as soon as the state is established. Authorizing content can be
provided in a Blazor Server app's AuthorizeView component, but the content is never
displayed.

[Authorize] attribute
The [Authorize] attribute is available in Razor components:

razor

@page "/"
@attribute [Authorize]

You can only see this if you're signed in.

) Important

Only use [Authorize] on @page components reached via the Blazor Router.
Authorization is only performed as an aspect of routing and not for child
components rendered within a page. To authorize the display of specific parts
within a page, use AuthorizeView instead.

The [Authorize] attribute also supports role-based or policy-based authorization. For


role-based authorization, use the Roles parameter:

razor

@page "/"
@attribute [Authorize(Roles = "Admin, Superuser")]

<p>You can only see this if you're in the 'Admin' or 'Superuser' role.</p>

For policy-based authorization, use the Policy parameter:

razor

@page "/"
@attribute [Authorize(Policy = "Over21")]

<p>You can only see this if you satisfy the 'Over21' policy.</p>

If neither Roles nor Policy is specified, [Authorize] uses the default policy:

Authenticated (signed-in) users are authorized.


Unauthenticated (signed-out) users are unauthorized.

When the user isn't authorized and if the app doesn't customize unauthorized content
with the Router component, the framework automatically displays the following fallback
message:

HTML

Not authorized.

Resource authorization
To authorize users for resources, pass the request's route data to the Resource
parameter of AuthorizeRouteView.

In the Router.Found content for a requested route in the App component ( App.razor ):

razor
<AuthorizeRouteView Resource="@routeData" RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />

For more information on how authorization state data is passed and used in procedural
logic, see the Expose the authentication state as a cascading parameter section.

When the AuthorizeRouteView receives the route data for the resource, authorization
policies have access to RouteData.PageType and RouteData.RouteValues that permit
custom logic to make authorization decisions.

In the following example, an EditUser policy is created in AuthorizationOptions for the


app's authorization service configuration (AddAuthorizationCore) with the following
logic:

Determine if a route value exists with a key of id . If the key exists, the route value
is stored in value .
In a variable named id , store value as a string or set an empty string value
( string.Empty ).
If id isn't an empty string, assert that the policy is satisfied (return true ) if the
string's value starts with EMP . Otherwise, assert that the policy fails (return false ).

In either Program.cs or Startup.cs (depending on the hosting model and framework


version):

Add namespaces for Microsoft.AspNetCore.Components and System.Linq:

C#

using Microsoft.AspNetCore.Components;
using System.Linq;

Add the policy:

C#

options.AddPolicy("EditUser", policy =>


policy.RequireAssertion(context =>
{
if (context.Resource is RouteData rd)
{
var routeValue = rd.RouteValues.TryGetValue("id", out var
value);
var id = Convert.ToString(value,
System.Globalization.CultureInfo.InvariantCulture) ??
string.Empty;
if (!string.IsNullOrEmpty(id))
{
return id.StartsWith("EMP",
StringComparison.InvariantCulture);
}
}

return false;
})
);

The preceding example is an oversimplified authorization policy, merely used to


demonstrate the concept with a working example. For more information on creating and
configuring authorization policies, see Policy-based authorization in ASP.NET Core.

In the following EditUser component, the resource at /users/{id}/edit has a route


parameter for the user's identifier ( {id} ). The component uses the preceding EditUser
authorization policy to determine if the route value for id starts with EMP . If id starts
with EMP , the policy succeeds and access to the component is authorized. If id starts
with a value other than EMP or if id is an empty string, the policy fails, and the
component doesn't load.

Pages/EditUser.razor :

razor

@page "/users/{id}/edit"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "EditUser")]

<h1>Edit User</h1>

<p>The "EditUser" policy is satisfied! <code>Id</code> starts with 'EMP'.


</p>

@code {
[Parameter]
public string? Id { get; set; }
}

Customize unauthorized content with the


Router component
The Router component, in conjunction with the AuthorizeRouteView component, allows
the app to specify custom content if:
The user fails an [Authorize] condition applied to the component. The markup of
the <NotAuthorized> element is displayed. The [Authorize] attribute is covered in
the [Authorize] attribute section.
Asynchronous authorization is in progress, which usually means that the process of
authenticating the user is in progress. The markup of the <Authorizing> element is
displayed.
Content isn't found. The markup of the <NotFound> element is displayed.

In the App component ( App.razor ):

razor

<CascadingAuthenticationState>
<Router ...>
<Found ...>
<AuthorizeRouteView ...>
<NotAuthorized>
...
</NotAuthorized>
<Authorizing>
...
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView ...>
...
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>

The content of <NotFound> , <NotAuthorized> , and <Authorizing> tags can include


arbitrary items, such as other interactive components.

If the <NotAuthorized> tag isn't specified, the AuthorizeRouteView uses the following
fallback message:

HTML

Not authorized.

An app created from the Blazor WebAssembly project template with authentication
enabled includes a RedirectToLogin component (Shared/RedirectToLogin.razor reference
source) , which is positioned in the <NotAuthorized> content of the Blazor Router in
the template's App component (reference source) . When a user isn't authenticated
( context.User.Identity?.IsAuthenticated != true ), the RedirectToLogin component
redirects the browser to the authentication/login endpoint for authentication. The user
is returned to the requested URL after authenticating with the identity provider.

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Procedural logic
If the app is required to check authorization rules as part of procedural logic, use a
cascaded parameter of type Task< AuthenticationState > to obtain the user's
ClaimsPrincipal. Task< AuthenticationState > can be combined with other services, such
as IAuthorizationService , to evaluate policies.

In the following example:

The user.Identity.IsAuthenticated executes code for authenticated (signed-in)


users.
The user.IsInRole("admin") executes code for users in the 'Admin' role.
The (await AuthorizationService.AuthorizeAsync(user, "content-
editor")).Succeeded executes code for users satisfying the 'content-editor' policy.

A Blazor Server app includes the appropriate namespaces by default when created from
the Blazor Server project template. In a Blazor WebAssembly app, confirm the presence
of the Microsoft.AspNetCore.Authorization and
Microsoft.AspNetCore.Components.Authorization namespaces either in the component
or in the app's _Imports.razor file:

razor

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization

Pages/ProceduralLogic.razor :

razor
@page "/procedural-logic"
@inject IAuthorizationService AuthorizationService

<h1>Procedural Logic Example</h1>

<button @onclick="@DoSomething">Do something important</button>

@code {
[CascadingParameter]
private Task<AuthenticationState>? authenticationState { get; set; }

private async Task DoSomething()


{
if (authenticationState is not null)
{
var authState = await authenticationState;
var user = authState?.User;

if (user is not null)


{
if (user.Identity is not null &&
user.Identity.IsAuthenticated)
{
// ...
}

if (user.IsInRole("Admin"))
{
// ...
}

if ((await AuthorizationService.AuthorizeAsync(user,
"content-editor"))
.Succeeded)
{
// ...
}
}
}
}
}

Troubleshoot errors
Common errors:

Authorization requires a cascading parameter of type


Task<AuthenticationState> . Consider using CascadingAuthenticationState to

supply this.
null value is received for authenticationStateTask

It's likely that the project wasn't created using a Blazor Server template with
authentication enabled. Wrap a <CascadingAuthenticationState> around some part of
the UI tree, for example in the App component ( App.razor ) as follows:

razor

<CascadingAuthenticationState>
<Router ...>
...
</Router>
</CascadingAuthenticationState>

The CascadingAuthenticationState supplies the Task< AuthenticationState > cascading


parameter, which in turn it receives from the underlying AuthenticationStateProvider
dependency injection service.

Additional resources
Microsoft identity platform documentation
Overview
OAuth 2.0 and OpenID Connect protocols on the Microsoft identity platform
Microsoft identity platform and OAuth 2.0 authorization code flow
Microsoft identity platform ID tokens
Microsoft identity platform access tokens
ASP.NET Core security topics
Configure Windows Authentication in ASP.NET Core
Build a custom version of the Authentication.MSAL JavaScript library
Awesome Blazor: Authentication community sample links
ASP.NET Core Blazor Hybrid authentication and authorization
Secure ASP.NET Core Blazor Server apps
Article • 04/04/2023

This article explains how to secure Blazor Server apps as ASP.NET Core applications.

Blazor Server apps are configured for security in the same manner as ASP.NET Core
apps. For more information, see the articles under ASP.NET Core security topics. Topics
under this overview apply specifically to Blazor Server.

In Blazor Server apps, the authentication context is only established when the app starts,
which is when the app first connects to the WebSocket. The authentication context is
maintained for the lifetime of the circuit. Blazor Server apps periodically revalidate the
user's authentication state, currently every 30 minutes by default.

If the app must capture users for custom services or react to updates to the user, see
ASP.NET Core Blazor Server additional security scenarios.

Blazor Server differs from a traditional server-rendered web apps that make new HTTP
requests with cookies on every page navigation. Authentication is checked during
navigation events. However, cookies aren't involved. Cookies are only sent when making
an HTTP request to a server, which isn't what happens when the user navigates in a
Blazor Server app. During navigation, the user's authentication state is checked within
the Blazor circuit, which you can update at any time on the server using the
RevalidatingAuthenticationStateProvider abstraction.

) Important

Implementing a custom NavigationManager to achieve authentication validation


during navigation isn't recommended for Blazor Server apps. If the app must
execute custom authentication state logic during navigation, use a custom
AuthenticationStateProvider.

7 Note

The code examples in this article adopt nullable reference types (NRTs) and .NET
compiler null-state static analysis, which are supported in ASP.NET Core 6.0 or
later. When targeting ASP.NET Core 5.0 or earlier, remove the null type designation
( ? ) from the examples in this article.
Blazor Server project template
The Blazor Server project template can be configured for authentication when the
project is created.

Visual Studio

Follow the Visual Studio guidance in Tooling for ASP.NET Core Blazor to create a
new Blazor Server project with an authentication mechanism.

After choosing the Blazor Server App template and configuring the project, select
the app's authentication under Authentication type:

Individual Accounts: User accounts are stored within the app using ASP.NET
Core Identity.
Microsoft identity platform: For more information, see ASP.NET Core Blazor
authentication and authorization.
Windows: Use Windows Authentication.

Scaffold Identity
For more information on scaffolding Identity into a Blazor Server project, see Scaffold
Identity in ASP.NET Core projects.

Additional claims and tokens from external


providers
To store additional claims from external providers, see Persist additional claims and
tokens from external providers in ASP.NET Core.

Azure App Service on Linux with Identity Server


Specify the issuer explicitly when deploying to Azure App Service on Linux with Identity
Server. For more information, see Introduction to authentication for Single Page Apps
on ASP.NET Core.
Implement a custom
AuthenticationStateProvider
If the app requires a custom provider, implement AuthenticationStateProvider and
override GetAuthenticationStateAsync.

In the following example, all users are authenticated with the username mrfibuli .

CustomAuthenticationStateProvider.cs :

C#

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthenticationStateProvider : AuthenticationStateProvider


{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "mrfibuli"),
}, "Custom Authentication");

var user = new ClaimsPrincipal(identity);

return Task.FromResult(new AuthenticationState(user));


}
}

The CustomAuthenticationStateProvider service is registered in Program.cs after the call


to AddServerSideBlazor:

C#

using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddServerSideBlazor();

...

builder.Services.AddScoped<AuthenticationStateProvider,
CustomAuthenticationStateProvider>();
Confirm or add an AuthorizeRouteView and CascadingAuthenticationState to the App
component.

In App.razor :

razor

<CascadingAuthenticationState>
<Router ...>
<Found ...>
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
...
</Found>
<NotFound>
...
</NotFound>
</Router>
</CascadingAuthenticationState>

7 Note

When you create a Blazor app from one of the Blazor project templates with
authentication enabled, the App component includes the AuthorizeRouteView and
CascadingAuthenticationState components shown in the preceding example. For
more information, see ASP.NET Core Blazor authentication and authorization with
additional information presented in the article's Customize unauthorized content
with the Router component section.

An AuthorizeView demonstrates the authenticated user's name in any component:

razor

<AuthorizeView>
<Authorized>
<p>Hello, @context.User.Identity?.Name!</p>
</Authorized>
<NotAuthorized>
<p>You're not authorized.</p>
</NotAuthorized>
</AuthorizeView>

For guidance on the use of AuthorizeView, see ASP.NET Core Blazor authentication and
authorization.
Notification about authentication state
changes
A custom AuthenticationStateProvider can invoke NotifyAuthenticationStateChanged on
the AuthenticationStateProvider base class to notify consumers of the authentication
state change to rerender.

The following example is based on implementing a custom AuthenticationStateProvider


by following the guidance in the Implement a custom AuthenticationStateProvider
section.

The following CustomAuthenticationStateProvider implementation exposes a custom


method, AuthenticateUser , to sign in a user and notify consumers of the authentication
state change.

CustomAuthenticationStateProvider.cs :

C#

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthenticationStateProvider : AuthenticationStateProvider


{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity();
var user = new ClaimsPrincipal(identity);

return Task.FromResult(new AuthenticationState(user));


}

public void AuthenticateUser(string userIdentifier)


{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, userIdentifier),
}, "Custom Authentication");

var user = new ClaimsPrincipal(identity);

NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(user)));
}
}

In a component:
Inject AuthenticationStateProvider.
Add a field to hold the user's identifier.
Add a button and a method to cast the AuthenticationStateProvider to
CustomAuthenticationStateProvider and call AuthenticateUser with the user's
identifier.

razor

@inject AuthenticationStateProvider AuthenticationStateProvider

<input @bind="userIdentifier" />


<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
<Authorized>
<p>Hello, @context.User.Identity?.Name!</p>
</Authorized>
<NotAuthorized>
<p>You're not authorized.</p>
</NotAuthorized>
</AuthorizeView>

@code {
public string userIdentifier = string.Empty;

private void SignIn()


{
((CustomAuthenticationStateProvider)AuthenticationStateProvider)
.AuthenticateUser(userIdentifier);
}
}

The preceding approach can be enhanced to trigger notifications of authentication state


changes via a custom service. The following AuthenticationService maintains the
current user's claims principal in a backing field ( currentUser ) with an event
( UserChanged ) that the AuthenticationStateProvider can subscribe to, where the event
invokes NotifyAuthenticationStateChanged. With the additional configuration later in
this section, the AuthenticationService can be injected into a component with logic that
sets the CurrentUser to trigger the UserChanged event.

C#

using System.Security.Claims;

public class AuthenticationService


{
public event Action<ClaimsPrincipal>? UserChanged;
private ClaimsPrincipal? currentUser;
public ClaimsPrincipal CurrentUser
{
get { return currentUser ?? new(); }
set
{
currentUser = value;

if (UserChanged is not null)


{
UserChanged(currentUser);
}
}
}
}

In Program.cs , register the AuthenticationService in the dependency injection


container:

C#

builder.Services.AddScoped<AuthenticationService>();

The following CustomAuthenticationStateProvider subscribes to the


AuthenticationService.UserChanged event. GetAuthenticationStateAsync returns the
authentication state of the service's current user ( AuthenticationService.CurrentUser ).

C#

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthenticationStateProvider : AuthenticationStateProvider


{
private AuthenticationState authenticationState;

public CustomAuthenticationStateProvider(AuthenticationService service)


{
authenticationState = new AuthenticationState(service.CurrentUser);

service.UserChanged += (newUser) =>


{
NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(newUser)));
};
}

public override Task<AuthenticationState> GetAuthenticationStateAsync()


=>
Task.FromResult(authenticationState);
}

The following component's SignIn method creates a claims principal for the user's
identifier to set on AuthenticationService.CurrentUser :

razor

@inject AuthenticationService AuthenticationService

<input @bind="userIdentifier" />


<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
<Authorized>
<p>Hello, @context.User.Identity?.Name!</p>
</Authorized>
<NotAuthorized>
<p>You're not authorized.</p>
</NotAuthorized>
</AuthorizeView>

@code {
public string userIdentifier = string.Empty;

private void SignIn()


{
var currentUser = AuthenticationService.CurrentUser;

var identity = new ClaimsIdentity(


new[]
{
new Claim(ClaimTypes.Name, userIdentifier),
},
"Custom Authentication");

var newUser = new ClaimsPrincipal(identity);

AuthenticationService.CurrentUser = newUser;
}
}

Inject AuthenticationStateProvider for services


scoped to a component
Don't attempt to resolve AuthenticationStateProvider within a custom scope because it
results in the creation of a new instance of the AuthenticationStateProvider that isn't
correctly initialized.
To access the AuthenticationStateProvider within a service scoped to a component,
inject the AuthenticationStateProvider with the @inject directive or the [Inject] attribute
and pass it to the service as a parameter. This approach ensures that the correct,
initialized instance of the AuthenticationStateProvider is used for each user app instance.

ExampleService.cs :

C#

public class ExampleService


{
public async Task<string> ExampleMethod(AuthenticationStateProvider
authStateProvider)
{
var authState = await
authStateProvider.GetAuthenticationStateAsync();
var user = authState.User;

if (user.Identity is not null && user.Identity.IsAuthenticated)


{
return $"{user.Identity.Name} is authenticated.";
}
else
{
return "The user is NOT authenticated.";
}
}
}

Register the service as scoped. In a Blazor Server app, scoped services have a lifetime
equal to the duration of the client connection circuit.

In Program.cs :

C#

builder.Services.AddScoped<ExampleService>();

In the following InjectAuthStateProvider component:

The component inherits OwningComponentBase.


The AuthenticationStateProvider is injected and passed to
ExampleService.ExampleMethod .

ExampleService is resolved with OwningComponentBase.ScopedServices and


GetRequiredService, which returns the correct, initialized instance of
ExampleService that exists for the lifetime of the user's circuit.
Pages/InjectAuthStateProvider.razor :

razor

@page "/inject-auth-state-provider"
@inject AuthenticationStateProvider AuthenticationStateProvider
@inherits OwningComponentBase

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
private string? message;
private ExampleService? ExampleService { get; set; }

protected override async Task OnInitializedAsync()


{
ExampleService = ScopedServices.GetRequiredService<ExampleService>
();

message = await
ExampleService.ExampleMethod(AuthenticationStateProvider);
}
}

For more information, see the guidance on OwningComponentBase in ASP.NET Core


Blazor dependency injection.

User state management


In spite of the word "state" in the name, AuthenticationStateProvider isn't for storing
general user state. AuthenticationStateProvider only indicates the user's authentication
state to the app, whether they are signed into the app and who they are signed in as.

In Blazor Server apps, authentication uses the same ASP.NET Core Identity
authentication as Razor Pages and MVC apps. The user state stored for ASP.NET Core
Identity flows to Blazor without adding additional code to the app. Follow the guidance
in the ASP.NET Core Identity articles and tutorials for the Identity features to take effect
in the Blazor parts of the app.

For guidance on general state management outside of ASP.NET Core Identity, see
ASP.NET Core Blazor state management.

Additional security abstractions


Two additional abstractions participate in managing authentication state:

ServerAuthenticationStateProvider (reference source ): An


AuthenticationStateProvider used by the Blazor framework to obtain
authentication state from the server.

RevalidatingServerAuthenticationStateProvider (reference source ): A base class


for AuthenticationStateProvider services used by the Blazor framework to receive
an authentication state from the host environment and revalidate it at regular
intervals.

For a Blazor Server app created from the Blazor Server project template with
authentication enabled, the default 30 minute revalidation interval can be adjusted
in RevalidatingIdentityAuthenticationStateProvider
(Areas/Identity/RevalidatingIdentityAuthenticationStateProvider.cs) . The
following example shortens the interval to 20 minutes:

C#

protected override TimeSpan RevalidationInterval =>


TimeSpan.FromMinutes(20);

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Additional resources
Quickstart: Add sign-in with Microsoft to an ASP.NET Core web app
Quickstart: Protect an ASP.NET Core web API with Microsoft identity platform
Configure ASP.NET Core to work with proxy servers and load balancers: Includes
guidance on:
Using Forwarded Headers Middleware to preserve HTTPS scheme information
across proxy servers and internal networks.
Additional scenarios and use cases, including manual scheme configuration,
request path changes for correct request routing, and forwarding the request
scheme for Linux and non-IIS reverse proxies.
Threat mitigation guidance for ASP.NET
Core Blazor Server
Article • 04/04/2023

This article explains how to mitigate security threats to Blazor Server apps.

Blazor Server apps adopt a stateful data processing model, where the server and client
maintain a long-lived relationship. The persistent state is maintained by a circuit, which
can span connections that are also potentially long-lived.

When a user visits a Blazor Server site, the server creates a circuit in the server's memory.
The circuit indicates to the browser what content to render and responds to events, such
as when the user selects a button in the UI. To perform these actions, a circuit invokes
JavaScript functions in the user's browser and .NET methods on the server. This two-way
JavaScript-based interaction is referred to as JavaScript interop (JS interop).

Because JS interop occurs over the Internet and the client uses a remote browser, Blazor
Server apps share most web app security concerns. This topic describes common threats
to Blazor Server apps and provides threat mitigation guidance focused on Internet-
facing apps.

In constrained environments, such as inside corporate networks or intranets, some of


the mitigation guidance either:

Doesn't apply in the constrained environment.


Isn't worth the cost to implement because the security risk is low in a constrained
environment.

Shared state
Blazor server apps live in server memory, and multiple app sessions are hosted within
the same process. For each app session, Blazor starts a circuit with its own dependency
injection container scope, thus scoped services are unique per Blazor session.

2 Warning

We don't recommend apps on the same server share state using singleton services
unless extreme care is taken, as this can introduce security vulnerabilities, such as
leaking user state across circuits.
You can use stateful singleton services in Blazor apps if they're specifically designed for
it. For example, use of a singleton memory cache is acceptable because a memory cache
requires a key to access a given entry. Assuming users don't have control over the cache
keys that are used with the cache, state stored in the cache doesn't leak across circuits.

For general guidance on state management, see ASP.NET Core Blazor state
management.

Avoid IHttpContextAccessor / HttpContext in


Razor components
Don't use IHttpContextAccessor/HttpContext directly or indirectly in the Razor
components of Blazor Server apps. Blazor apps run outside of the ASP.NET Core
pipeline context. The HttpContext isn't guaranteed to be available within the
IHttpContextAccessor, and HttpContext isn't guaranteed to hold the context that started
the Blazor app.

The recommended approach for passing request state to the Blazor app is through root
component parameters during the app's initial rendering. Alternatively, the app can
copy the data into a scoped service in the root component's initialization lifecycle event
for use across the app. For more information, see ASP.NET Core Blazor Server additional
security scenarios.

A critical aspect of Blazor Server security is that the user attached to a given circuit
might become updated at some point after the Blazor circuit is established but the
IHttpContextAccessor isn't updated. For more information on addressing this situation
with custom services, see ASP.NET Core Blazor Server additional security scenarios.

Resource exhaustion
Resource exhaustion can occur when a client interacts with the server and causes the
server to consume excessive resources. Excessive resource consumption primarily
affects:

CPU
Memory
Client connections

Denial of service (DoS) attacks usually seek to exhaust an app or server's resources.
However, resource exhaustion isn't necessarily the result of an attack on the system. For
example, finite resources can be exhausted due to high user demand. DoS is covered
further in the Denial of service (DoS) attacks section.

Resources external to the Blazor framework, such as databases and file handles (used to
read and write files), may also experience resource exhaustion. For more information,
see ASP.NET Core Best Practices.

CPU
CPU exhaustion can occur when one or more clients force the server to perform
intensive CPU work.

For example, consider a Blazor Server app that calculates a Fibonnacci number. A
Fibonnacci number is produced from a Fibonnacci sequence, where each number in the
sequence is the sum of the two preceding numbers. The amount of work required to
reach the answer depends on the length of the sequence and the size of the initial value.
If the app doesn't place limits on a client's request, the CPU-intensive calculations may
dominate the CPU's time and diminish the performance of other tasks. Excessive
resource consumption is a security concern impacting availability.

CPU exhaustion is a concern for all public-facing apps. In regular web apps, requests
and connections time out as a safeguard, but Blazor Server apps don't provide the same
safeguards. Blazor Server apps must include appropriate checks and limits before
performing potentially CPU-intensive work.

Memory
Memory exhaustion can occur when one or more clients force the server to consume a
large amount of memory.

For example, consider a Blazor-server side app with a component that accepts and
displays a list of items. If the Blazor app doesn't place limits on the number of items
allowed or the number of items rendered back to the client, the memory-intensive
processing and rendering may dominate the server's memory to the point where
performance of the server suffers. The server may crash or slow to the point that it
appears to have crashed.

Consider the following scenario for maintaining and displaying a list of items that
pertain to a potential memory exhaustion scenario on the server:

The items in a List<T> property or field use the server's memory. If the app allows
the list of items to grow unbounded, there's a risk of the server running out of
memory. Running out of memory causes the current session to end (crash) and all
of the concurrent sessions in that server instance receive an out-of-memory
exception. To prevent this scenario from occurring, the app must use a data
structure that imposes an item limit on concurrent users.
If a paging scheme isn't used for rendering, the server uses additional memory for
objects that aren't visible in the UI. Without a limit on the number of items,
memory demands may exhaust the available server memory. To prevent this
scenario, use one of the following approaches:
Use paginated lists when rendering.
Only display the first 100 to 1,000 items and require the user to enter search
criteria to find items beyond the items displayed.
For a more advanced rendering scenario, implement lists or grids that support
virtualization. Using virtualization, lists only render a subset of items currently
visible to the user. When the user interacts with the scrollbar in the UI, the
component renders only those items required for display. The items that aren't
currently required for display can be held in secondary storage, which is the
ideal approach. Undisplayed items can also be held in memory, which is less
ideal.

7 Note

Blazor has built-in support for virtualization. For more information, see ASP.NET
Core Razor component virtualization.

Blazor Server apps offer a similar programming model to other UI frameworks for
stateful apps, such as WPF, Windows Forms, or Blazor WebAssembly. The main
difference is that in several of the UI frameworks the memory consumed by the app
belongs to the client and only affects that individual client. For example, a Blazor
WebAssembly app runs entirely on the client and only uses client memory resources. In
the Blazor Server scenario, the memory consumed by the app belongs to the server and
is shared among clients on the server instance.

Server-side memory demands are a consideration for all Blazor Server apps. However,
most web apps are stateless, and the memory used while processing a request is
released when the response is returned. As a general recommendation, don't permit
clients to allocate an unbound amount of memory as in any other server-side app that
persists client connections. The memory consumed by a Blazor Server app persists for a
longer time than a single request.

7 Note
During development, a profiler can be used or a trace captured to assess memory
demands of clients. A profiler or trace won't capture the memory allocated to a
specific client. To capture the memory use of a specific client during development,
capture a dump and examine the memory demand of all the objects rooted at a
user's circuit.

Client connections
Connection exhaustion can occur when one or more clients open too many concurrent
connections to the server, preventing other clients from establishing new connections.

Blazor clients establish a single connection per session and keep the connection open
for as long as the browser window is open. Given the persistent nature of the
connections and the stateful nature of Blazor Server apps, connection exhaustion is a
greater risk to availability of the app.

By default, there's no limit on the number of connections per user for a Blazor Server
app. If the app requires a connection limit, take one or more of the following
approaches:

Require authentication, which naturally limits the ability of unauthorized users to


connect to the app. For this scenario to be effective, users must be prevented from
provisioning new users on demand.
Limit the number of connections per user. Limiting connections can be
accomplished via the following approaches. Exercise care to allow legitimate users
to access the app (for example, when a connection limit is established based on
the client's IP address).

At the app level:


Endpoint routing extensibility.
Require authentication to connect to the app and keep track of the active
sessions per user.
Reject new sessions upon reaching a limit.
Proxy WebSocket connections to an app through the use of a proxy, such as
the Azure SignalR Service that multiplexes connections from clients to an
app. This provides an app with greater connection capacity than a single
client can establish, preventing a client from exhausting the connections to
the server.

At the server level: Use a proxy/gateway in front of the app. For example, Azure
Front Door enables you to define, manage, and monitor the global routing of
web traffic to an app and works when Blazor Server apps are configured to use
Long Polling.

7 Note

Although Long Polling is supported for Blazor Server apps, WebSockets is


the recommended transport protocol. As of February, 2023, Azure Front
Door doesn't support WebSockets, but support for WebSockets is under
development for a future release of the service. For more information, see
Support WebSocket connections on Azure Front Door .

Denial of service (DoS) attacks


Denial of service (DoS) attacks involve a client causing the server to exhaust one or
more of its resources making the app unavailable. Blazor Server apps include default
limits and rely on other ASP.NET Core and SignalR limits that are set on CircuitOptions
to protect against DoS attacks:

CircuitOptions.DisconnectedCircuitMaxRetained
CircuitOptions.DisconnectedCircuitRetentionPeriod
CircuitOptions.JSInteropDefaultCallTimeout
CircuitOptions.MaxBufferedUnacknowledgedRenderBatches
HubConnectionContextOptions.MaximumReceiveMessageSize

For more information and configuration coding examples, see the following articles:

ASP.NET Core Blazor SignalR guidance


ASP.NET Core SignalR configuration

Interactions with the browser (client)


A client interacts with the server through JS interop event dispatching and render
completion. JS interop communication goes both ways between JavaScript and .NET:

Browser events are dispatched from the client to the server in an asynchronous
fashion.
The server responds asynchronously rerendering the UI as necessary.

JavaScript functions invoked from .NET


For calls from .NET methods to JavaScript:
All invocations have a configurable timeout after which they fail, returning a
OperationCanceledException to the caller.
There's a default timeout for the calls
(CircuitOptions.JSInteropDefaultCallTimeout) of one minute. To configure this
limit, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.
A cancellation token can be provided to control the cancellation on a per-call
basis. Rely on the default call timeout where possible and time-bound any call
to the client if a cancellation token is provided.
The result of a JavaScript call can't be trusted. The Blazor app client running in the
browser searches for the JavaScript function to invoke. The function is invoked, and
either the result or an error is produced. A malicious client can attempt to:
Cause an issue in the app by returning an error from the JavaScript function.
Induce an unintended behavior on the server by returning an unexpected result
from the JavaScript function.

Take the following precautions to guard against the preceding scenarios:

Wrap JS interop calls within try-catch statements to account for errors that might
occur during the invocations. For more information, see Handle errors in ASP.NET
Core Blazor apps.
Validate data returned from JS interop invocations, including error messages,
before taking any action.

.NET methods invoked from the browser


Don't trust calls from JavaScript to .NET methods. When a .NET method is exposed to
JavaScript, consider how the .NET method is invoked:

Treat any .NET method exposed to JavaScript as you would a public endpoint to
the app.
Validate input.
Ensure that values are within expected ranges.
Ensure that the user has permission to perform the action requested.
Don't allocate an excessive quantity of resources as part of the .NET method
invocation. For example, perform checks and place limits on CPU and memory
use.
Take into account that static and instance methods can be exposed to JavaScript
clients. Avoid sharing state across sessions unless the design calls for sharing
state with appropriate constraints.
For instance methods exposed through DotNetObjectReference objects that
are originally created through dependency injection (DI), the objects should
be registered as scoped objects. This applies to any DI service that the Blazor
Server app uses.
For static methods, avoid establishing state that can't be scoped to the client
unless the app is explicitly sharing state by-design across all users on a server
instance.
Avoid passing user-supplied data in parameters to JavaScript calls. If passing
data in parameters is absolutely required, ensure that the JavaScript code
handles passing the data without introducing Cross-site scripting (XSS)
vulnerabilities. For example, don't write user-supplied data to the Document
Object Model (DOM) by setting the innerHTML property of an element. Consider
using Content Security Policy (CSP) to disable eval and other unsafe
JavaScript primitives. For more information, see Enforce a Content Security
Policy for ASP.NET Core Blazor.
Avoid implementing custom dispatching of .NET invocations on top of the
framework's dispatching implementation. Exposing .NET methods to the browser is
an advanced scenario, not recommended for general Blazor development.

Events
Events provide an entry point to a Blazor Server app. The same rules for safeguarding
endpoints in web apps apply to event handling in Blazor Server apps. A malicious client
can send any data it wishes to send as the payload for an event.

For example:

A change event for a <select> could send a value that isn't within the options that
the app presented to the client.
An <input> could send any text data to the server, bypassing client-side validation.

The app must validate the data for any event that the app handles. The Blazor
framework forms components perform basic validations. If the app uses custom forms
components, custom code must be written to validate event data as appropriate.

Blazor Server events are asynchronous, so multiple events can be dispatched to the
server before the app has time to react by producing a new render. This has some
security implications to consider. Limiting client actions in the app must be performed
inside event handlers and not depend on the current rendered view state.

Consider a counter component that should allow a user to increment a counter a


maximum of three times. The button to increment the counter is conditionally based on
the value of count :
razor

<p>Count: @count</p>

@if (count < 3)


{
<button @onclick="IncrementCount" value="Increment count" />
}

@code
{
private int count = 0;

private void IncrementCount()


{
count++;
}
}

A client can dispatch one or more increment events before the framework produces a
new render of this component. The result is that the count can be incremented over
three times by the user because the button isn't removed by the UI quickly enough. The
correct way to achieve the limit of three count increments is shown in the following
example:

razor

<p>Count: @count</p>

@if (count < 3)


{
<button @onclick="IncrementCount" value="Increment count" />
}

@code
{
private int count = 0;

private void IncrementCount()


{
if (count < 3)
{
count++;
}
}
}

By adding the if (count < 3) { ... } check inside the handler, the decision to
increment count is based on the current app state. The decision isn't based on the state
of the UI as it was in the previous example, which might be temporarily stale.

Guard against multiple dispatches


If an event callback invokes a long running operation asynchronously, such as fetching
data from an external service or database, consider using a guard. The guard can
prevent the user from queueing up multiple operations while the operation is in
progress with visual feedback. The following component code sets isLoading to true
while DataService.GetDataAsync obtains data from the server. While isLoading is true ,
the button is disabled in the UI:

razor

<button disabled="@isLoading" @onclick="UpdateData">Update</button>

@code {
private bool isLoading;
private Data[] data = Array.Empty<Data>();

private async Task UpdateData()


{
if (!isLoading)
{
isLoading = true;
data = await DataService.GetDataAsync(DateTime.Now);
isLoading = false;
}
}
}

The guard pattern demonstrated in the preceding example works if the background
operation is executed asynchronously with the async - await pattern.

Cancel early and avoid use-after-dispose


In addition to using a guard as described in the Guard against multiple dispatches
section, consider using a CancellationToken to cancel long-running operations when the
component is disposed. This approach has the added benefit of avoiding use-after-
dispose in components:

razor

@implements IDisposable

...
@code {
private readonly CancellationTokenSource TokenSource =
new CancellationTokenSource();

private async Task UpdateData()


{
...

data = await DataService.GetDataAsync(DateTime.Now,


TokenSource.Token);

if (TokenSource.Token.IsCancellationRequested)
{
return;
}

...
}

public void Dispose()


{
TokenSource.Cancel();
}
}

Avoid events that produce large amounts of data


Some DOM events, such as oninput or onscroll , can produce a large amount of data.
Avoid using these events in Blazor server apps.

Additional security guidance


The guidance for securing ASP.NET Core apps apply to Blazor Server apps and are
covered in the following sections of this article:

Logging and sensitive data


Protect information in transit with HTTPS
Cross-site scripting (XSS)
Cross-origin protection
Click-jacking
Open redirects

Logging and sensitive data


JS interop interactions between the client and server are recorded in the server's logs
with ILogger instances. Blazor avoids logging sensitive information, such as actual
events or JS interop inputs and outputs.

When an error occurs on the server, the framework notifies the client and tears down
the session. By default, the client receives a generic error message that can be seen in
the browser's developer tools.

The client-side error doesn't include the call stack and doesn't provide detail on the
cause of the error, but server logs do contain such information. For development
purposes, sensitive error information can be made available to the client by enabling
detailed errors.

2 Warning

Exposing error information to clients on the Internet is a security risk that should
always be avoided.

Protect information in transit with HTTPS


Blazor Server uses SignalR for communication between the client and the server. Blazor
Server normally uses the transport that SignalR negotiates, which is typically
WebSockets.

Blazor Server doesn't ensure the integrity and confidentiality of the data sent between
the server and the client. Always use HTTPS.

Cross-site scripting (XSS)


Cross-site scripting (XSS) allows an unauthorized party to execute arbitrary logic in the
context of the browser. A compromised app could potentially run arbitrary code on the
client. The vulnerability could be used to potentially perform a number of malicious
actions against the server:

Dispatch fake/invalid events to the server.


Dispatch fail/invalid render completions.
Avoid dispatching render completions.
Dispatch interop calls from JavaScript to .NET.
Modify the response of interop calls from .NET to JavaScript.
Avoid dispatching .NET to JS interop results.

The Blazor Server framework takes steps to protect against some of the preceding
threats:
Stops producing new UI updates if the client isn't acknowledging render batches.
Configured with CircuitOptions.MaxBufferedUnacknowledgedRenderBatches.
Times out any .NET to JavaScript call after one minute without receiving a response
from the client. Configured with CircuitOptions.JSInteropDefaultCallTimeout.
Performs basic validation on all input coming from the browser during JS interop:
.NET references are valid and of the type expected by the .NET method.
The data isn't malformed.
The correct number of arguments for the method are present in the payload.
The arguments or result can be deserialized correctly before invoking the
method.
Performs basic validation in all input coming from the browser from dispatched
events:
The event has a valid type.
The data for the event can be deserialized.
There's an event handler associated with the event.

In addition to the safeguards that the framework implements, the app must be coded by
the developer to safeguard against threats and take appropriate actions:

Always validate data when handling events.


Take appropriate action upon receiving invalid data:
Ignore the data and return. This allows the app to continue processing requests.
If the app determines that the input is illegitimate and couldn't be produced by
legitimate client, throw an exception. Throwing an exception tears down the
circuit and ends the session.
Don't trust the error message provided by render batch completions included in
the logs. The error is provided by the client and can't generally be trusted, as the
client might be compromised.
Don't trust the input on JS interop calls in either direction between JavaScript and
.NET methods.
The app is responsible for validating that the content of arguments and results are
valid, even if the arguments or results are correctly deserialized.

For a XSS vulnerability to exist, the app must incorporate user input in the rendered
page. Blazor Server components execute a compile-time step where the markup in a
.razor file is transformed into procedural C# logic. At runtime, the C# logic builds a

render tree describing the elements, text, and child components. This is applied to the
browser's DOM via a sequence of JavaScript instructions (or is serialized to HTML in the
case of prerendering):

User input rendered via normal Razor syntax (for example, @someStringValue )
doesn't expose a XSS vulnerability because the Razor syntax is added to the DOM
via commands that can only write text. Even if the value includes HTML markup,
the value is displayed as static text. When prerendering, the output is HTML-
encoded, which also displays the content as static text.
Script tags aren't allowed and shouldn't be included in the app's component
render tree. If a script tag is included in a component's markup, a compile-time
error is generated.
Component authors can author components in C# without using Razor. The
component author is responsible for using the correct APIs when emitting output.
For example, use builder.AddContent(0, someUserSuppliedString) and not
builder.AddMarkupContent(0, someUserSuppliedString) , as the latter could create a

XSS vulnerability.

Consider further mitigating XSS vulnerabilities. For example, implement a restrictive


Content Security Policy (CSP) . For more information, see Enforce a Content Security
Policy for ASP.NET Core Blazor.

For more information, see Prevent Cross-Site Scripting (XSS) in ASP.NET Core.

Cross-origin protection
Cross-origin attacks involve a client from a different origin performing an action against
the server. The malicious action is typically a GET request or a form POST (Cross-Site
Request Forgery, CSRF), but opening a malicious WebSocket is also possible. Blazor
Server apps offer the same guarantees that any other SignalR app using the hub
protocol offer:

Blazor Server apps can be accessed cross-origin unless additional measures are
taken to prevent it. To disable cross-origin access, either disable CORS in the
endpoint by adding the CORS Middleware to the pipeline and adding the
DisableCorsAttribute to the Blazor endpoint metadata or limit the set of allowed
origins by configuring SignalR for cross-origin resource sharing. For guidance on
WebSocket origin restrictions, see WebSockets support in ASP.NET Core.
If CORS is enabled, extra steps might be required to protect the app depending on
the CORS configuration. If CORS is globally enabled, CORS can be disabled for the
Blazor Server hub by adding the DisableCorsAttribute metadata to the endpoint
metadata after calling MapBlazorHub on the endpoint route builder.

For more information, see Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in
ASP.NET Core.

Click-jacking
Click-jacking involves rendering a site as an <iframe> inside a site from a different origin
in order to trick the user into performing actions on the site under attack.

To protect an app from rendering inside of an <iframe> , use Content Security Policy
(CSP) and the X-Frame-Options header.

For more information, see the following resources:

Enforce a Content Security Policy for ASP.NET Core Blazor


MDN web docs: X-Frame-Options

Open redirects
When a Blazor Server app session starts, the server performs basic validation of the URLs
sent as part of starting the session. The framework checks that the base URL is a parent
of the current URL before establishing the circuit. No additional checks are performed
by the framework.

When a user selects a link on the client, the URL for the link is sent to the server, which
determines what action to take. For example, the app may perform a client-side
navigation or indicate to the browser to go to the new location.

Components can also trigger navigation requests programmatically through the use of
NavigationManager. In such scenarios, the app might perform a client-side navigation
or indicate to the browser to go to the new location.

Components must:

Avoid using user input as part of the navigation call arguments.


Validate arguments to ensure that the target is allowed by the app.

Otherwise, a malicious user can force the browser to go to an attacker-controlled site. In


this scenario, the attacker tricks the app into using some user input as part of the
invocation of the NavigationManager.NavigateTo method.

This advice also applies when rendering links as part of the app:

If possible, use relative links.


Validate that absolute link destinations are valid before including them in a page.

For more information, see Prevent open redirect attacks in ASP.NET Core.

Security checklist
The following list of security considerations isn't comprehensive:

Validate arguments from events.


Validate inputs and results from JS interop calls.
Avoid using (or validate beforehand) user input for .NET to JS interop calls.
Prevent the client from allocating an unbound amount of memory.
Data within the component.
DotNetObjectReference objects returned to the client.
Guard against multiple dispatches.
Cancel long-running operations when the component is disposed.
Avoid events that produce large amounts of data.
Avoid using user input as part of calls to NavigationManager.NavigateTo and
validate user input for URLs against a set of allowed origins first if unavoidable.
Don't make authorization decisions based on the state of the UI but only from
component state.
Consider using Content Security Policy (CSP) to protect against XSS attacks. For
more information, see Enforce a Content Security Policy for ASP.NET Core Blazor.
Consider using CSP and X-Frame-Options to protect against click-jacking.
Ensure CORS settings are appropriate when enabling CORS or explicitly disable
CORS for Blazor apps.
Test to ensure that the server-side limits for the Blazor app provide an acceptable
user experience without unacceptable levels of risk.
ASP.NET Core Blazor Server additional
security scenarios
Article • 06/01/2023

This article explains how to configure Blazor Server for additional security scenarios,
including how to pass tokens to a Blazor Server app.

Pass tokens to a Blazor Server app


Tokens available outside of the Razor components in a Blazor Server app can be passed
to components with the approach described in this section. The example in this section
focuses on passing access, refresh, and anti-request forgery (XSRF) token tokens to the
Blazor app, but the approach is valid for other HTTP context state.

7 Note

Passing the XSRF token to Razor components is useful in scenarios where


components POST to Identity or other endpoints that require validation. If your app
only requires access and refresh tokens, you can remove the XSRF token code from
the following example.

Authenticate the Blazor Server app as you would with a regular Razor Pages or MVC
app. Provision and save the tokens to the authentication cookie.

In Program.cs :

C#

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

builder.Services.Configure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;

options.Scope.Add("offline_access");
});
Optionally, additional scopes are added with options.Scope.Add("{SCOPE}"); , where the
placeholder {SCOPE} is the additional scope to add.

Define a scoped token provider service that can be used within the Blazor app to resolve
the tokens from dependency injection (DI).

TokenProvider.cs :

C#

public class TokenProvider


{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}

In Program.cs , add services for:

IHttpClientFactory: Used in a WeatherForecastService class that obtains weather


data from a server API with an access token.
TokenProvider : Holds the access and refresh tokens.

C#

builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

Define a class to pass in the initial app state with the access and refresh tokens.

InitialApplicationState.cs :

C#

public class InitialApplicationState


{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}

In the Pages/_Host.cshtml file, create and instance of InitialApplicationState and pass


it as a parameter to the app:

CSHTML
@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf

...

@{
var tokens = new InitialApplicationState
{
AccessToken = await HttpContext.GetTokenAsync("access_token"),
RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
};
}

<component ... param-InitialState="tokens" ... />

In the App component ( App.razor ), resolve the service and initialize it with the data
from the parameter:

razor

@inject TokenProvider TokenProvider

...

@code {
[Parameter]
public InitialApplicationState? InitialState { get; set; }

protected override Task OnInitializedAsync()


{
TokenProvider.AccessToken = InitialState?.AccessToken;
TokenProvider.RefreshToken = InitialState?.RefreshToken;
TokenProvider.XsrfToken = InitialState?.XsrfToken;

return base.OnInitializedAsync();
}
}

7 Note

An alternative to assigning the initial state to the TokenProvider in the preceding


example is to copy the data into a scoped service within OnInitializedAsync for use
across the app.

Add a package reference to the app for the Microsoft.AspNet.WebApi.Client NuGet


package.
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 .

In the service that makes a secure API request, inject the token provider and retrieve the
token for the API request:

WeatherForecastService.cs :

C#

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class WeatherForecastService


{
private readonly HttpClient http;
private readonly TokenProvider tokenProvider;

public WeatherForecastService(IHttpClientFactory clientFactory,


TokenProvider tokenProvider)
{
http = clientFactory.CreateClient();
this.tokenProvider = tokenProvider;
}

public async Task<WeatherForecast[]> GetForecastAsync()


{
var token = tokenProvider.AccessToken;
var request = new HttpRequestMessage(HttpMethod.Get,
"https://localhost:5003/WeatherForecast");
request.Headers.Add("Authorization", $"Bearer {token}");
var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<WeatherForecast[]>()


??
Array.Empty<WeatherForecast>();
}
}

For an XSRF token passed to a component, inject the TokenProvider and add the XSRF
token to the POST request. The following example adds the token to a logout endpoint
POST. The scenario for the following example is that the logout endpoint
( Areas/Identity/Pages/Account/Logout.cshtml , scaffolded into the app) doesn't specify
an IgnoreAntiforgeryTokenAttribute ( @attribute [IgnoreAntiforgeryToken] ) because it
performs some action in addition to a normal logout operation that must be protected.
The endpoint requires a valid XSRF token to successfully process the request.

In a component that presents a Logout button to authorized users:

razor

@inject TokenProvider TokenProvider

...

<AuthorizeView>
<Authorized>
<form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
<button class="nav-link btn btn-link"
type="submit">Logout</button>
<input name="__RequestVerificationToken" type="hidden"
value="@TokenProvider.XsrfToken">
</form>
</Authorized>
<NotAuthorized>
...
</NotAuthorized>
</AuthorizeView>

Set the authentication scheme


For an app that uses more than one Authentication Middleware and thus has more than
one authentication scheme, the scheme that Blazor uses can be explicitly set in the
endpoint configuration of Program.cs . The following example sets the OpenID Connect
(OIDC) scheme:

C#

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapBlazorHub().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
});
Circuit handler to capture users for custom
services
Use a CircuitHandler to capture a user from the AuthenticationStateProvider and set the
user in a service. If you want to update the user, register a callback to
AuthenticationStateChanged and queue a Task to obtain the new user and update the
service. The following example demonstrates the approach.

In the following example:

OnConnectionUpAsync is called every time the circuit reconnects, setting the user
for the lifetime of the connection. Only the OnConnectionUpAsync method is
required unless you implement updates via a handler for authentication changes
( AuthenticationChanged in the following example).
OnCircuitOpenedAsync is called to attach the authentication changed handler,
AuthenticationChanged , to update the user.

The catch block of the UpdateAuthentication task takes no action on exceptions


because there's no way to report them at this point in code execution. If an
exception is thrown from the task, the exception is reported elsewhere in app.

UserService.cs :

C#

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService


{
private ClaimsPrincipal currentUser = new(new ClaimsIdentity());

public ClaimsPrincipal GetUser()


{
return currentUser;
}

internal void SetUser(ClaimsPrincipal user)


{
if (currentUser != user)
{
currentUser = user;
}
}
}

internal sealed class UserCircuitHandler : CircuitHandler, IDisposable


{
private readonly AuthenticationStateProvider
authenticationStateProvider;
private readonly UserService userService;

public UserCircuitHandler(
AuthenticationStateProvider authenticationStateProvider,
UserService userService)
{
this.authenticationStateProvider = authenticationStateProvider;
this.userService = userService;
}

public override Task OnCircuitOpenedAsync(Circuit circuit,


CancellationToken cancellationToken)
{
authenticationStateProvider.AuthenticationStateChanged +=
AuthenticationChanged;

return base.OnCircuitOpenedAsync(circuit, cancellationToken);


}

private void AuthenticationChanged(Task<AuthenticationState> task)


{
_ = UpdateAuthentication(task);

async Task UpdateAuthentication(Task<AuthenticationState> task)


{
try
{
var state = await task;
userService.SetUser(state.User);
}
catch
{
}
}
}

public override async Task OnConnectionUpAsync(Circuit circuit,


CancellationToken cancellationToken)
{
var state = await
authenticationStateProvider.GetAuthenticationStateAsync();
userService.SetUser(state.User);
}

public void Dispose()


{
authenticationStateProvider.AuthenticationStateChanged -=
AuthenticationChanged;
}
}
In Program.cs :

C#

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

Use the service in a component to obtain the user:

razor

@inject UserService UserService

<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>

To set the user in middleware for MVC, Razor Pages, and in other ASP.NET Core
scenarios, call SetUser on the UserService in custom middleware after the
Authentication Middleware runs, or set the user with an IClaimsTransformation
implementation. The following example adopts the middleware approach.

UserServiceMiddleware.cs :

C#

public class UserServiceMiddleware


{
private readonly RequestDelegate next;

public UserServiceMiddleware(RequestDelegate next)


{
this.next = next ?? throw new ArgumentNullException(nameof(next));
}

public async Task InvokeAsync(HttpContext context, UserService service)


{
service.SetUser(context.User);
await next(context);
}
}

Immediately before the call to app.MapBlazorHub() in Program.cs , call the middleware:


C#

app.UseMiddleware<UserServiceMiddleware>();
Secure ASP.NET Core Blazor
WebAssembly
Article • 04/04/2023

Blazor WebAssembly apps are secured in the same manner as single-page applications
(SPAs). There are several approaches for authenticating users to SPAs, but the most
common and comprehensive approach is to use an implementation based on the OAuth
2.0 protocol , such as OpenID Connect (OIDC) .

The Blazor WebAssembly security documentation primarily focuses on how to


accomplish user authentication and authorization tasks. For OAuth 2.0/OIDC general
concept coverage, see the resources in the main overview article's Additional resources
section.

Authentication library
Blazor WebAssembly supports authenticating and authorizing apps using OIDC via the
Microsoft.AspNetCore.Components.WebAssembly.Authentication library. The library
provides a set of primitives for seamlessly authenticating against ASP.NET Core
backends. The library integrates ASP.NET Core Identity with API authorization support
built on top of Duende Identity Server . The library can authenticate against any third-
party Identity Provider (IP) that supports OIDC, which are called OpenID Providers (OP).

The authentication support in Blazor WebAssembly is built on top of the OIDC Client
Library ( oidc-client.js ), which is used to handle the underlying authentication protocol
details.

Other options for authenticating SPAs exist, such as the use of SameSite cookies.
However, the engineering design of Blazor WebAssembly uses OAuth and OIDC as the
best option for authentication in Blazor WebAssembly apps. Token-based authentication
based on JSON Web Tokens (JWTs) was chosen over cookie-based authentication for
functional and security reasons:

Using a token-based protocol offers a smaller attack surface area, as the tokens
aren't sent in all requests.
Server endpoints don't require protection against Cross-Site Request Forgery
(CSRF) because the tokens are sent explicitly. This allows you to host Blazor
WebAssembly apps alongside MVC or Razor pages apps.
Tokens have narrower permissions than cookies. For example, tokens can't be used
to manage the user account or change a user's password unless such functionality
is explicitly implemented.
Tokens have a short lifetime, one hour by default, which limits the attack window.
Tokens can also be revoked at any time.
Self-contained JWTs offer guarantees to the client and server about the
authentication process. For example, a client has the means to detect and validate
that the tokens it receives are legitimate and were emitted as part of a given
authentication process. If a third party attempts to switch a token in the middle of
the authentication process, the client can detect the switched token and avoid
using it.
Tokens with OAuth and OIDC don't rely on the user agent behaving correctly to
ensure that the app is secure.
Token-based protocols, such as OAuth and OIDC, allow for authenticating and
authorizing users of hosted Blazor WebAssembly solution clients and standalone
Blazor Webassembly apps with the same set of security characteristics.

) Important

For versions of ASP.NET Core that adopt Duende Identity Server in Blazor project
templates, 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.

Authentication process with OIDC


The Microsoft.AspNetCore.Components.WebAssembly.Authentication library offers
several primitives to implement authentication and authorization using OIDC. In broad
terms, authentication works as follows:

When an anonymous user selects the login button or requests a Razor component
or page with the [Authorize] attribute applied, the user is redirected to the app's
login page ( /authentication/login ).
In the login page, the authentication library prepares for a redirect to the
authorization endpoint. The authorization endpoint is outside of the Blazor
WebAssembly app and can be hosted at a separate origin. The endpoint is
responsible for determining whether the user is authenticated and for issuing one
or more tokens in response. The authentication library provides a login callback to
receive the authentication response.
If the user isn't authenticated, the user is redirected to the underlying
authentication system, which is usually ASP.NET Core Identity.
If the user was already authenticated, the authorization endpoint generates the
appropriate tokens and redirects the browser back to the login callback
endpoint ( /authentication/login-callback ).
When the Blazor WebAssembly app loads the login callback endpoint
( /authentication/login-callback ), the authentication response is processed.
If the authentication process completes successfully, the user is authenticated
and optionally sent back to the original protected URL that the user requested.
If the authentication process fails for any reason, the user is sent to the login
failed page ( /authentication/login-failed ), where an error is displayed.

Authentication component
The Authentication component ( Pages/Authentication.razor ) handles remote
authentication operations and permits the app to:

Configure app routes for authentication states.


Set UI content for authentication states.
Manage authentication state.

Authentication actions, such as registering or signing in a user, are passed to the Blazor
framework's RemoteAuthenticatorViewCore<TAuthenticationState> component, which
persists and controls state across authentication operations.

For more information and examples, see ASP.NET Core Blazor WebAssembly additional
security scenarios.

Authorization
In Blazor WebAssembly apps, authorization checks can be bypassed because all client-
side code can be modified by users. The same is true for all client-side app technologies,
including JavaScript SPA frameworks or native apps for any operating system.

Always perform authorization checks on the server within any API endpoints accessed
by your client-side app.

Customize authentication
Blazor WebAssembly provides methods to add and retrieve additional parameters for
the underlying Authentication library to conduct remote authentication operations with
external identity providers.
To pass additional parameters, NavigationManager supports passing and retrieving
history entry state when performing external location changes. For more information,
see the following resources:

Blazor Fundamentals > Routing and navigation article


Navigation history state
Navigation options
MDN documentation: History API

The state stored by the History API provides the following benefits for remote
authentication:

The state passed to the secured app endpoint is tied to the navigation performed
to authenticate the user at the authentication/login endpoint.
Extra work encoding and decoding data is avoided.
The attack surface area is reduced. Unlike using the query string to store
navigation state, a top-level navigation or influence from a different origin can't set
the state stored by the History API.
The history entry is replaced upon successful authentication, so the state attached
to the history entry is removed and doesn't require clean up.

InteractiveRequestOptions represents the request to the identity provider for logging in


or provisioning an access token.

NavigationManagerExtensions provides the NavigateToLogin method for a login


operation and NavigateToLogout for a logout operation. The methods call
NavigationManager.NavigateTo, setting the history entry state with a passed
InteractiveRequestOptions or a new InteractiveRequestOptions instance created by the
method for:

A user signing in (InteractionType.SignIn) with the current URI for the return URL.
A user signing out (InteractionType.SignOut) with the return URL.

The following authentication scenarios are covered in the ASP.NET Core Blazor
WebAssembly additional security scenarios article:

Customize the login process


Logout with a custom return URL
Customize options before obtaining a token interactively
Customize options when using an IAccessTokenProvider
Obtain the login path from authentication options

Require authorization for the entire app


Apply the [Authorize] attribute (API documentation) to each Razor component of the
app using one of the following approaches:

In the app's Imports file, add an @using directive for the


Microsoft.AspNetCore.Authorization namespace with an @attribute directive for
the [Authorize] attribute.

_Imports.razor :

razor

@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

Allow anonymous access to the Authentication component to permit redirection


to the identity provider. Add the following Razor code to the Authentication
component under its @page directive.

Pages/Authentication.razor :

razor

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@attribute [AllowAnonymous]

Add the attribute to each Razor component in the Pages folder under their @page
directives:

razor

@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

7 Note

Setting an AuthorizationOptions.FallbackPolicy to a policy with


RequireAuthenticatedUser is not supported.

Use one identity provider app registration per


app
Some of the articles under this Overview pertain to either of the following Blazor hosting
scenarios that involve two or more apps:

A hosted Blazor WebAssembly solution, which is composed of two apps: a client-


side Blazor WebAssembly app and a server-side ASP.NET Core host app.
Authenticated users to the client app access server resources and data provided by
the server app.
A standalone Blazor WebAssembly app that uses web API with authenticated users
to access server resources and data provided by a server app. This scenario is
similar to using a hosted Blazor WebAssembly solution; but in this case, the client
app isn't hosted by the server app.

When these scenarios are implemented in documentation examples, two identity


provider registrations are used, one for the client app and one for the server app. Using
separate registrations, for example in Azure Active Directory, isn't strictly required.
However, using two registrations is a security best practice because it isolates the
registrations by app. Using separate registrations also allows independent configuration
of the client and server registrations.

Refresh tokens
Although refresh tokens can't be secured in Blazor WebAssembly apps, they can be used
if you implement them with appropriate security strategies.

For standalone Blazor WebAssembly apps in ASP.NET Core 6.0 or later, we recommend
using:

The OAuth 2.0 Authorization Code flow (Code) with Proof Key for Code Exchange
(PKCE) .
A refresh token that has a short expiration.
A rotated refresh token.
A refresh token with an expiration after which a new interactive authorization flow
is required to refresh the user's credentials.

For hosted Blazor WebAssembly solutions, refresh tokens can be maintained and used
by the server-side app in order to access third-party APIs. For more information, see
ASP.NET Core Blazor WebAssembly additional security scenarios.

For more information, see the following resources:

Microsoft identity platform refresh tokens: Refresh token lifetime


OAuth 2.0 for Browser-Based Apps (IETF specification)
Establish claims for users
Apps often require claims for users based on a web API call to a server. For example,
claims are frequently used to establish authorization in an app. In these scenarios, the
app requests an access token to access the service and uses the token to obtain user
data for creating claims.

For examples, see the following resources:

Additional scenarios: Customize the user


ASP.NET Core Blazor WebAssembly with Azure Active Directory groups and roles

Prerendering support
Prerendering isn't supported for authentication endpoints ( /authentication/ path
segment).

For more information, see ASP.NET Core Blazor WebAssembly additional security
scenarios.

Azure App Service on Linux with Identity Server


Specify the issuer explicitly when deploying to Azure App Service on Linux with Identity
Server.

For more information, see Introduction to authentication for Single Page Apps on
ASP.NET Core.

Windows Authentication
We don't recommend using Windows Authentication with Blazor Webassembly or with
any other SPA framework. We recommend using token-based protocols instead of
Windows Authentication, such as OIDC with Active Directory Federation Services (ADFS).

If Windows Authentication is used with Blazor Webassembly or with any other SPA
framework, additional measures are required to protect the app from cross-site request
forgery (CSRF) tokens. The same concerns that apply to cookies apply to Windows
Authentication with the addition that Windows Authentication doesn't offer a
mechanism to prevent sharing of the authentication context across origins. Apps using
Windows Authentication without additional protection from CSRF should at least be
restricted to an organization's intranet and not be used on the open Internet.
For more information, see Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in
ASP.NET Core.

Secure a SignalR hub


To secure a SignalR hub:

In the Server project, apply the [Authorize] attribute to the hub class or to
methods of the hub class.

In the Client project's component, supply an access token to the hub connection:

razor

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject NavigationManager Navigation

...

var tokenResult = await TokenProvider.RequestAccessToken();

if (tokenResult.TryGetToken(out var token))


{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"),
options => { options.AccessTokenProvider = () =>
Task.FromResult(token?.Value); })
.Build();

...
}

For more information, see Authentication and authorization in ASP.NET Core SignalR.

Logging
This section applies to Blazor WebAssembly apps in ASP.NET Core 7.0 or later.

To enable debug or trace logging, see the Authentication logging (Blazor WebAssembly)
section in a 7.0 or later version of the ASP.NET Core Blazor logging article.

The WebAssembly sandbox


The WebAssembly sandbox restricts access to the environment of the system executing
WebAssembly code, including access to I/O subsystems, system storage and resources,
and the operating system. The isolation between WebAssembly code and the system
that executes the code makes WebAssembly a safe coding framework for systems.
However, WebAssembly is vulnerable to side-channel attacks at the hardware level.
Normal precautions and due diligence in sourcing hardware and placing limitations on
accessing hardware apply.

WebAssembly isn't owned or maintained by Microsoft.

For more information, see the following W3C resources:

WebAssembly: Security
WebAssembly Specification: Security Considerations
W3C WebAssembly Community Group: Feedback and issues : The W3C
WebAssembly Community Group link is only provided for reference, making it
clear that WebAssembly security vulnerabilities and bugs are patched on an
ongoing basis, often reported and addressed by browser. Don't send feedback or
bug reports on Blazor to the W3C WebAssembly Community Group. Blazor
feedback should be reported to the Microsoft ASP.NET Core product unit . If the
Microsoft product unit determines that an underlying problem with WebAssembly
exists, the product unit will take the appropriate steps to report the problem to the
W3C WebAssembly Community Group.

Implementation guidance
Articles under this Overview provide information on authenticating users in Blazor
WebAssembly apps against specific providers.

Standalone Blazor WebAssembly apps:

General guidance for OIDC providers and the WebAssembly Authentication Library
Microsoft Accounts
Azure Active Directory (AAD)
Azure Active Directory (AAD) B2C

Hosted Blazor WebAssembly apps:

Azure Active Directory (AAD)


Azure Active Directory (AAD) B2C
Identity Server

Further configuration guidance is found in the following articles:


ASP.NET Core Blazor WebAssembly additional security scenarios
Use Graph API with ASP.NET Core Blazor WebAssembly

Use the Authorization Code flow with PKCE


Microsoft identity platform's Microsoft Authentication Library for JavaScript (MSAL) v2.0
or later provides support for the Authorization Code flow with Proof Key for Code
Exchange (PKCE) and cross-origin resource sharing (CORS) for single-page
applications, including Blazor.

Microsoft doesn't recommend using Implicit grant.

For more information, see the following resources:

Authentication flow support in MSAL: Implicit grant


Microsoft identity platform and implicit grant flow: Prefer the auth code flow
Microsoft identity platform and OAuth 2.0 authorization code flow

Additional resources
Microsoft identity platform documentation
General documentation
Access tokens
Configure ASP.NET Core to work with proxy servers and load balancers
Using Forwarded Headers Middleware to preserve HTTPS scheme information
across proxy servers and internal networks.
Additional scenarios and use cases, including manual scheme configuration,
request path changes for correct request routing, and forwarding the request
scheme for Linux and non-IIS reverse proxies.
Prerendering with authentication
WebAssembly: Security
WebAssembly Specification: Security Considerations
Secure an ASP.NET Core Blazor
WebAssembly standalone app with the
Authentication library
Article • 03/15/2023

This article explains how to secure an ASP.NET Core Blazor WebAssembly standalone
app with the Blazor WebAssembly Authentication library.

For Azure Active Directory (AAD) and Azure Active Directory B2C (AAD B2C) guidance,
don't follow the guidance in this topic. See Secure an ASP.NET Core Blazor WebAssembly
standalone app with Azure Active Directory or Secure an ASP.NET Core Blazor
WebAssembly standalone app with Azure Active Directory B2C.

For additional security scenario coverage after reading this article, see ASP.NET Core
Blazor WebAssembly additional security scenarios.

Walkthrough
The subsections of the walkthrough explain how to:

Register an app
Create the Blazor app
Run the app

Register an app
Register an app with an OpenID Connect (OIDC) Identity Provider (IP) following the
guidance provided by the maintainer of the IP.

Record the following information:

Authority (for example, https://accounts.google.com/ ).


Application (client) ID (for example, 2...7-e...q.apps.googleusercontent.com ).
Additional IP configuration (see the IP's documentation).

7 Note

The IP must use OIDC. For example, Facebook's IP isn't an OIDC-compliant


provider, so the guidance in this topic doesn't work with the Facebook IP. For more
information, see Secure ASP.NET Core Blazor WebAssembly.

Create the Blazor app


To create a standalone Blazor WebAssembly app that uses the
Microsoft.AspNetCore.Components.WebAssembly.Authentication library, follow the
guidance for your choice of tooling. If adding support for authentication, see the Parts
of the app section of this article for guidance on setting up and configuring the app.

Visual Studio

To create a new Blazor WebAssembly project with an authentication mechanism:

After choosing the Blazor WebAssembly App template, set the Authentication
type to Individual Accounts. Confirm that the ASP.NET Core Hosted checkbox is
not selected.

The Individual Accounts selection uses ASP.NET Core's Identity system. This
selection adds authentication support and doesn't result in storing users in a
database. The following sections of this article provide further details.

Configure the app


Configure the app following the IP's guidance. At a minimum, the app requires the
Local:Authority and Local:ClientId configuration settings in the app's

wwwroot/appsettings.json file:

JSON

{
"Local": {
"Authority": "{AUTHORITY}",
"ClientId": "{CLIENT ID}"
}
}

Google OAuth 2.0 OIDC example for an app that runs on the localhost address at port
5001:

JSON
{
"Local": {
"Authority": "https://accounts.google.com/",
"ClientId": "2...7-e...q.apps.googleusercontent.com",
"PostLogoutRedirectUri": "https://localhost:5001/authentication/logout-
callback",
"RedirectUri": "https://localhost:5001/authentication/login-callback",
"ResponseType": "id_token"
}
}

The redirect URI ( https://localhost:5001/authentication/login-callback ) is registered


in the Google APIs console in Credentials > {NAME} > Authorized redirect URIs,
where {NAME} is the app's client name in the OAuth 2.0 Client IDs app list of the Google
APIs console.

7 Note

Supplying the port number for a localhost redirect URI isn't required for some
OIDC IPs per the OAuth 2.0 specification . Some IPs permit the redirect URI for
loopback addresses to omit the port. Others allow the use of a wildcard for the port
number (for example, * ). For additional information, see the IP's documentation.

Run the app


Use one of the following approaches to run the app:

Visual Studio
Select the Run button.
Use Debug > Start Debugging from the menu.
Press F5 .
.NET CLI command shell: Execute the dotnet run command from the app's folder.

Parts of the app


This section describes the parts of an app generated from the Blazor WebAssembly
project template and how the app is configured. There's no specific guidance to follow
in this section for a basic working application if you created the app using the guidance
in the Walkthrough section. The guidance in this section is helpful for updating an app
to authenticate and authorize users. However, an alternative approach to updating an
app is to create a new app from the guidance in the Walkthrough section and moving
the app's components, classes, and resources to the new app.

Authentication package
When an app is created to use Individual User Accounts, the app automatically receives
a package reference for the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package. The
package provides a set of primitives that help the app authenticate users and obtain
tokens to call protected APIs.

If adding authentication to an app, manually add the


Microsoft.AspNetCore.Components.WebAssembly.Authentication package to the app.

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 .

Authentication service support


Support for authenticating users using OpenID Connect (OIDC) is registered in the
service container with the AddOidcAuthentication extension method provided by the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package.

The AddOidcAuthentication method accepts a callback to configure the parameters


required to authenticate an app using OIDC. The values required for configuring the app
can be obtained from the OIDC-compliant IP. Obtain the values when you register the
app, which typically occurs in their online portal.

For a new app, provide values for the {AUTHORITY} and {CLIENT ID} placeholders in the
following configuration. Provide other configuration values that are required for use
with the app's IP. The example is for Google, which requires PostLogoutRedirectUri ,
RedirectUri , and ResponseType . If adding authentication to an app, manually add the

following code and configuration to the app with values for the placeholders and other
configuration values.

Program.cs :

C#
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Local", options.ProviderOptions);
});

wwwroot/appsettings.json configuration

Configuration is supplied by the wwwroot/appsettings.json file:

JSON

{
"Local": {
"Authority": "{AUTHORITY}",
"ClientId": "{CLIENT ID}"
}
}

Access token scopes


The Blazor WebAssembly template automatically configures default scopes for openid
and profile .

The Blazor WebAssembly template doesn't automatically configure the app to request
an access token for a secure API. To provision an access token as part of the sign-in flow,
add the scope to the default token scopes of the OidcProviderOptions. If adding
authentication to an app, manually add the following code and configure the scope URI.

Program.cs :

C#

builder.Services.AddOidcAuthentication(options =>
{
...
options.ProviderOptions.DefaultScopes.Add("{SCOPE URI}");
});

For more information, see the following sections of the Additional scenarios article:

Request additional access tokens


Attach tokens to outgoing requests

Imports file
The Microsoft.AspNetCore.Components.Authorization namespace is made available
throughout the app via the _Imports.razor file:

razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index page
The Index page ( wwwroot/index.html ) page includes a script that defines the
AuthenticationService in JavaScript. AuthenticationService handles the low-level

details of the OIDC protocol. The app internally calls methods defined in the script to
perform the authentication operations.

HTML

<script
src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/Aut
henticationService.js"></script>

App component
The App component ( App.razor ) is similar to the App component found in Blazor Server
apps:

The CascadingAuthenticationState component manages exposing the


AuthenticationState to the rest of the app.
The AuthorizeRouteView component makes sure that the current user is
authorized to access a given page or otherwise renders the RedirectToLogin
component.
The RedirectToLogin component manages redirecting unauthorized users to the
login page.
Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
App component ( App.razor ) isn't shown in this section. To inspect the markup of the
component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the App component ( App.razor ) in the generated app.

Inspect the App component ( App.razor ) in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

RedirectToLogin component
The RedirectToLogin component ( Shared/RedirectToLogin.razor ):

Manages redirecting unauthorized users to the login page.


The current URL that the user is attempting to access is maintained by so that they
can be returned to that page if authentication is successful using:
Navigation history state in ASP.NET Core 7.0 or later.
A query string in ASP.NET Core 6.0 or earlier.

Inspect the RedirectToLogin component in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

LoginDisplay component
The LoginDisplay component ( Shared/LoginDisplay.razor ) is rendered in the
MainLayout component ( Shared/MainLayout.razor ) and manages the following
behaviors:

For authenticated users:


Displays the current user name.
Offers a link to the user profile page in ASP.NET Core Identity.
Offers a button to log out of the app.
For anonymous users:
Offers the option to register.
Offers the option to log in.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
LoginDisplay component isn't shown in this section. To inspect the markup of the
component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the LoginDisplay component in the generated app.

Inspect the LoginDisplay component in reference source . The templated


content for Hosted equal to true is used.

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

Authentication component
The page produced by the Authentication component ( Pages/Authentication.razor )
defines the routes required for handling different authentication stages.

The RemoteAuthenticatorView component:

Is provided by the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package.
Manages performing the appropriate actions at each stage of authentication.
razor

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
[Parameter]
public string? Action { get; set; }
}

7 Note

Nullable reference types (NRTs) and .NET compiler null-state static analysis is
supported in ASP.NET Core 6.0 or later. Prior to the release of ASP.NET Core 6.0, the
string type appears without the null type designation ( ? ).

Troubleshoot

Logging
This section applies to ASP.NET Core 7.0 or later.

To enable debug or trace logging for Blazor WebAssembly authentication, see ASP.NET
Core Blazor logging.

Common errors
Misconfiguration of the app or Identity Provider (IP)

The most common errors are caused by incorrect configuration. The following are
a few examples:
Depending on the requirements of the scenario, a missing or incorrect
Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI
prevents an app from authenticating clients.
An incorrect access token scope prevents clients from accessing server web API
endpoints.
Incorrect or missing server API permissions prevent clients from accessing
server web API endpoints.
Running the app at a different port than is configured in the Redirect URI of the
Identity Provider's app registration.
Configuration sections of this article's guidance show examples of the correct
configuration. Carefully check each section of the article looking for app and IP
misconfiguration.

If the configuration appears correct:

Analyze application logs.

Examine the network traffic between the client app and the IP or server app with
the browser's developer tools. Often, an exact error message or a message with
a clue to what's causing the problem is returned to the client by the IP or server
app after making a request. Developer tools guidance is found in the following
articles:
Google Chrome (Google documentation)
Microsoft Edge
Mozilla Firefox (Mozilla documentation)

Decode the contents of a JSON Web Token (JWT) used for authenticating a
client or accessing a server web API, depending on where the problem is
occurring. For more information, see Inspect the content of a JSON Web Token
(JWT).

The documentation team responds to document feedback and bugs in articles


(open an issue from the This page feedback section) but is unable to provide
product support. Several public support forums are available to assist with
troubleshooting an app. We recommend the following:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

The preceding forums are not owned or controlled by Microsoft.

For non-security, non-sensitive, and non-confidential reproducible framework bug


reports, open an issue with the ASP.NET Core product unit . Don't open an issue
with the product unit until you've thoroughly investigated the cause of a problem
and can't resolve it on your own and with the help of the community on a public
support forum. The product unit isn't able to troubleshoot individual apps that are
broken due to simple misconfiguration or use cases involving third-party services.
If a report is sensitive or confidential in nature or describes a potential security flaw
in the product that attackers may exploit, see Reporting security issues and bugs
(dotnet/aspnetcore GitHub repository) .

Unauthorized client for AAD


info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

Login callback error from AAD:


Error: unauthorized_client
Description: AADB2C90058: The provided application is not configured to
allow public clients.

To resolve the error:

1. In the Azure portal, access the app's manifest.


2. Set the allowPublicClient attribute to null or true .

Cookies and site data


Cookies and site data can persist across app updates and interfere with testing and
troubleshooting. Clear the following when making app code changes, user account
changes with the provider, or provider app configuration changes:

User sign-in cookies


App cookies
Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing
and troubleshooting is to:

Configure a browser
Use a browser for testing that you can configure to delete all cookie and site
data each time the browser is closed.
Make sure that the browser is closed manually or by the IDE for any change to
the app, test user, or provider configuration.
Use a custom command to open a browser in incognito or private mode in Visual
Studio:
Open Browse With dialog box from Visual Studio's Run button.
Select the Add button.
Provide the path to your browser in the Program field. The following executable
paths are typical installation locations for Windows 10. If your browser is
installed in a different location or you aren't using Windows 10, provide the
path to the browser's executable.
Microsoft Edge: C:\Program Files
(x86)\Microsoft\Edge\Application\msedge.exe
Google Chrome: C:\Program Files
(x86)\Google\Chrome\Application\chrome.exe
Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe
In the Arguments field, provide the command-line option that the browser uses
to open in incognito or private mode. Some browsers require the URL of the
app.
Microsoft Edge: Use -inprivate .
Google Chrome: Use --incognito --new-window {URL} , where the placeholder
{URL} is the URL to open (for example, https://localhost:5001 ).
Mozilla Firefox: Use -private -url {URL} , where the placeholder {URL} is the
URL to open (for example, https://localhost:5001 ).
Provide a name in the Friendly name field. For example, Firefox Auth Testing .
Select the OK button.
To avoid having to select the browser profile for each iteration of testing with an
app, set the profile as the default with the Set as Default button.
Make sure that the browser is closed by the IDE for any change to the app, test
user, or provider configuration.

App upgrades
A functioning app may fail immediately after upgrading either the .NET Core SDK on the
development machine or changing package versions within the app. In some cases,
incoherent packages may break an app when performing major upgrades. Most of these
issues can be fixed by following these instructions:

1. Clear the local system's NuGet package caches by executing dotnet nuget locals all
--clear from a command shell.
2. Delete the project's bin and obj folders.
3. Restore and rebuild the project.
4. Delete all of the files in the deployment folder on the server prior to redeploying
the app.

7 Note

Use of package versions incompatible with the app's target framework isn't
supported. For information on a package, use the NuGet Gallery or FuGet
Package Explorer .

Run the Server app


When testing and troubleshooting a hosted Blazor WebAssembly solution, make sure
that you're running the app from the Server project.

Inspect the user


The ASP.NET Core framework's test assets include a Blazor WebAssembly client app
with a User component that can be useful in troubleshooting. The User component can
be used directly in apps or serve as the basis for further customization:

User test component in the dotnet/aspnetcore GitHub repository

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Inspect the content of a JSON Web Token (JWT)


To decode a JSON Web Token (JWT), use Microsoft's jwt.ms tool. Values in the UI
never leave your browser.

Example encoded JWT (shortened for display):

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0
djhkbE5QNC1j ...
bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzp
D-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-
yBMKV2_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

JSON

{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1610059429,
"nbf": 1610055829,
"ver": "1.0",
"iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-
226dcc9ad298/v2.0/",
"sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
"aud": "70bde375-fce3-4b82-984a-b247d823a03f",
"nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
"iat": 1610055829,
"auth_time": 1610055822,
"idp": "idp.com",
"tfp": "B2C_1_signupsignin"
}.[Signature]

Additional resources
ASP.NET Core Blazor WebAssembly additional security scenarios
Unauthenticated or unauthorized web API requests in an app with a secure default
client
Configure ASP.NET Core to work with proxy servers and load balancers: Includes
guidance on:
Using Forwarded Headers Middleware to preserve HTTPS scheme information
across proxy servers and internal networks.
Additional scenarios and use cases, including manual scheme configuration,
request path changes for correct request routing, and forwarding the request
scheme for Linux and non-IIS reverse proxies.
Secure an ASP.NET Core Blazor
WebAssembly standalone app with
Microsoft Accounts
Article • 04/25/2023

This article explains how to create a standalone Blazor WebAssembly app that uses
Microsoft Accounts with Azure Active Directory (AAD) for authentication.

For additional security scenario coverage after reading this article, see ASP.NET Core
Blazor WebAssembly additional security scenarios.

Walkthrough
The subsections of the walkthrough explain how to:

Create a tenant in Azure


Register an app in Azure
Create the Blazor app
Run the app

Create a tenant in Azure


Follow the guidance in Quickstart: Set up a tenant to create a tenant in AAD.

Register an app in Azure


Register an AAD app:

1. Navigate to Azure Active Directory in the Azure portal. Select App registrations in
the sidebar. Select the New registration button.
2. Provide a Name for the app (for example, Blazor Standalone AAD MS Accounts).
3. In Supported account types, select Accounts in any organizational directory (Any
Azure AD directory – Multitenant).
4. Set the Redirect URI dropdown list to Single-page application (SPA) and provide
the following redirect URI: https://localhost/authentication/login-callback . If
you know the production redirect URI for the Azure default host (for example,
azurewebsites.net ) or the custom domain host (for example, contoso.com ), you

can also add the production redirect URI at the same time that you're providing
the localhost redirect URI. Be sure to include the port number for non- :443 ports
in any production redirect URIs that you add.
5. If you're using an unverified publisher domain, clear the Permissions > Grant
admin consent to openid and offline_access permissions checkbox. If the
publisher domain is verified, this checkbox isn't present.
6. Select Register.

7 Note

Supplying the port number for a localhost AAD redirect URI isn't required. For
more information, see Redirect URI (reply URL) restrictions and limitations:
Localhost exceptions (Azure documentation).

Record the Application (client) ID (for example, 41451fa7-82d9-4673-8fa5-69eff5a761fd ).

In Authentication > Platform configurations > Single-page application:

1. Confirm the redirect URI of https://localhost/authentication/login-callback is


present.
2. In the Implicit grant section, ensure that the checkboxes for Access tokens and ID
tokens aren't selected. Implicit grant isn't recommended for Blazor apps using
MSAL v2.0 or later. For more information, see Secure ASP.NET Core Blazor
WebAssembly.
3. The remaining defaults for the app are acceptable for this experience.
4. Select the Save button if you made changes.

Create the Blazor app


Create the app. Replace the placeholders in the following command with the
information recorded earlier and execute the following command in a command shell:

.NET CLI

dotnet new blazorwasm -au SingleOrg --client-id "{CLIENT ID}" --tenant-id


"common" -o {PROJECT NAME}

Placeholder Azure portal name Example

{PROJECT NAME} — BlazorSample

{CLIENT ID} Application (client) ID 41451fa7-82d9-4673-8fa5-69eff5a761fd


The output location specified with the -o|--output option creates a project folder if it
doesn't exist and becomes part of the project's name.

Add a pair of MsalProviderOptions for openid and offline_access


DefaultAccessTokenScopes:

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});

Run the app


Use one of the following approaches to run the app:

Visual Studio
Select the Run button.
Use Debug > Start Debugging from the menu.
Press F5 .
.NET CLI command shell: Execute the dotnet run command from the app's folder.

Parts of the app


This section describes the parts of an app generated from the Blazor WebAssembly
project template and how the app is configured. There's no specific guidance to follow
in this section for a basic working application if you created the app using the guidance
in the Walkthrough section. The guidance in this section is helpful for updating an app
to authenticate and authorize users. However, an alternative approach to updating an
app is to create a new app from the guidance in the Walkthrough section and moving
the app's components, classes, and resources to the new app.

Authentication package
When an app is created to use Work or School Accounts ( SingleOrg ), the app
automatically receives a package reference for the Microsoft Authentication Library
(Microsoft.Authentication.WebAssembly.Msal ). The package provides a set of
primitives that help the app authenticate users and obtain tokens to call protected APIs.
If adding authentication to an app, manually add the
Microsoft.Authentication.WebAssembly.Msal package to the app.

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 .

The Microsoft.Authentication.WebAssembly.Msal package transitively adds the


Microsoft.AspNetCore.Components.WebAssembly.Authentication package to the app.

Authentication service support


Support for authenticating users is registered in the service container with the
AddMsalAuthentication extension method provided by the
Microsoft.Authentication.WebAssembly.Msal package. This method sets up all of the
services required for the app to interact with the Identity Provider (IP).

Program.cs :

C#

builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
});

The AddMsalAuthentication method accepts a callback to configure the parameters


required to authenticate an app. The values required for configuring the app can be
obtained from the AAD configuration when you register the app.

wwwroot/appsettings.json configuration

Configuration is supplied by the wwwroot/appsettings.json file:

JSON

{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/common",
"ClientId": "{CLIENT ID}",
"ValidateAuthority": true
}
}

Example:

JSON

{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/common",
"ClientId": "41451fa7-82d9-4673-8fa5-69eff5a761fd",
"ValidateAuthority": true
}
}

Access token scopes


The Blazor WebAssembly template doesn't automatically configure the app to request
an access token for a secure API. To provision an access token as part of the sign-in flow,
add the scope to the default access token scopes of the MsalProviderOptions:

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

Specify additional scopes with AdditionalScopesToConsent :

C#

options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");

For more information, see the following sections of the Additional scenarios article:

Request additional access tokens


Attach tokens to outgoing requests
Quickstart: Configure an application to expose web APIs

Login mode
The framework defaults to pop-up login mode and falls back to redirect login mode if a
pop-up can't be opened. Configure MSAL to use redirect login mode by setting the
LoginMode property of MsalProviderOptions to redirect :

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.LoginMode = "redirect";
});

The default setting is popup , and the string value isn't case-sensitive.

Imports file
The Microsoft.AspNetCore.Components.Authorization namespace is made available
throughout the app via the _Imports.razor file:

razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index page
The Index page ( wwwroot/index.html ) page includes a script that defines the
AuthenticationService in JavaScript. AuthenticationService handles the low-level
details of the OIDC protocol. The app internally calls methods defined in the script to
perform the authentication operations.

HTML

<script
src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationServic
e.js"></script>
App component
The App component ( App.razor ) is similar to the App component found in Blazor Server
apps:

The CascadingAuthenticationState component manages exposing the


AuthenticationState to the rest of the app.
The AuthorizeRouteView component makes sure that the current user is
authorized to access a given page or otherwise renders the RedirectToLogin
component.
The RedirectToLogin component manages redirecting unauthorized users to the
login page.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
App component ( App.razor ) isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the App component ( App.razor ) in the generated app.

Inspect the App component ( App.razor ) in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

RedirectToLogin component
The RedirectToLogin component ( Shared/RedirectToLogin.razor ):

Manages redirecting unauthorized users to the login page.


The current URL that the user is attempting to access is maintained by so that they
can be returned to that page if authentication is successful using:
Navigation history state in ASP.NET Core 7.0 or later.
A query string in ASP.NET Core 6.0 or earlier.
Inspect the RedirectToLogin component in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

LoginDisplay component
The LoginDisplay component ( Shared/LoginDisplay.razor ) is rendered in the
MainLayout component ( Shared/MainLayout.razor ) and manages the following
behaviors:

For authenticated users:


Displays the current user name.
Offers a link to the user profile page in ASP.NET Core Identity.
Offers a button to log out of the app.
For anonymous users:
Offers the option to register.
Offers the option to log in.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
LoginDisplay component isn't shown in this section. To inspect the markup of the
component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the LoginDisplay component in the generated app.

Inspect the LoginDisplay component in reference source . The templated


content for Hosted equal to true is used.

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

Authentication component
The page produced by the Authentication component ( Pages/Authentication.razor )
defines the routes required for handling different authentication stages.

The RemoteAuthenticatorView component:

Is provided by the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package.
Manages performing the appropriate actions at each stage of authentication.

razor

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
[Parameter]
public string? Action { get; set; }
}

7 Note

Nullable reference types (NRTs) and .NET compiler null-state static analysis is
supported in ASP.NET Core 6.0 or later. Prior to the release of ASP.NET Core 6.0, the
string type appears without the null type designation ( ? ).

Troubleshoot

Logging
This section applies to ASP.NET Core 7.0 or later.

To enable debug or trace logging for Blazor WebAssembly authentication, see ASP.NET
Core Blazor logging.
Common errors
Misconfiguration of the app or Identity Provider (IP)

The most common errors are caused by incorrect configuration. The following are
a few examples:
Depending on the requirements of the scenario, a missing or incorrect
Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI
prevents an app from authenticating clients.
An incorrect access token scope prevents clients from accessing server web API
endpoints.
Incorrect or missing server API permissions prevent clients from accessing
server web API endpoints.
Running the app at a different port than is configured in the Redirect URI of the
Identity Provider's app registration.

Configuration sections of this article's guidance show examples of the correct


configuration. Carefully check each section of the article looking for app and IP
misconfiguration.

If the configuration appears correct:

Analyze application logs.

Examine the network traffic between the client app and the IP or server app with
the browser's developer tools. Often, an exact error message or a message with
a clue to what's causing the problem is returned to the client by the IP or server
app after making a request. Developer tools guidance is found in the following
articles:
Google Chrome (Google documentation)
Microsoft Edge
Mozilla Firefox (Mozilla documentation)

Decode the contents of a JSON Web Token (JWT) used for authenticating a
client or accessing a server web API, depending on where the problem is
occurring. For more information, see Inspect the content of a JSON Web Token
(JWT).

The documentation team responds to document feedback and bugs in articles


(open an issue from the This page feedback section) but is unable to provide
product support. Several public support forums are available to assist with
troubleshooting an app. We recommend the following:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

The preceding forums are not owned or controlled by Microsoft.

For non-security, non-sensitive, and non-confidential reproducible framework bug


reports, open an issue with the ASP.NET Core product unit . Don't open an issue
with the product unit until you've thoroughly investigated the cause of a problem
and can't resolve it on your own and with the help of the community on a public
support forum. The product unit isn't able to troubleshoot individual apps that are
broken due to simple misconfiguration or use cases involving third-party services.
If a report is sensitive or confidential in nature or describes a potential security flaw
in the product that attackers may exploit, see Reporting security issues and bugs
(dotnet/aspnetcore GitHub repository) .

Unauthorized client for AAD

info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

Login callback error from AAD:


Error: unauthorized_client
Description: AADB2C90058: The provided application is not configured to
allow public clients.

To resolve the error:

1. In the Azure portal, access the app's manifest.


2. Set the allowPublicClient attribute to null or true .

Cookies and site data


Cookies and site data can persist across app updates and interfere with testing and
troubleshooting. Clear the following when making app code changes, user account
changes with the provider, or provider app configuration changes:

User sign-in cookies


App cookies
Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing
and troubleshooting is to:
Configure a browser
Use a browser for testing that you can configure to delete all cookie and site
data each time the browser is closed.
Make sure that the browser is closed manually or by the IDE for any change to
the app, test user, or provider configuration.
Use a custom command to open a browser in incognito or private mode in Visual
Studio:
Open Browse With dialog box from Visual Studio's Run button.
Select the Add button.
Provide the path to your browser in the Program field. The following executable
paths are typical installation locations for Windows 10. If your browser is
installed in a different location or you aren't using Windows 10, provide the
path to the browser's executable.
Microsoft Edge: C:\Program Files
(x86)\Microsoft\Edge\Application\msedge.exe

Google Chrome: C:\Program Files


(x86)\Google\Chrome\Application\chrome.exe
Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe
In the Arguments field, provide the command-line option that the browser uses
to open in incognito or private mode. Some browsers require the URL of the
app.
Microsoft Edge: Use -inprivate .
Google Chrome: Use --incognito --new-window {URL} , where the placeholder
{URL} is the URL to open (for example, https://localhost:5001 ).
Mozilla Firefox: Use -private -url {URL} , where the placeholder {URL} is the
URL to open (for example, https://localhost:5001 ).
Provide a name in the Friendly name field. For example, Firefox Auth Testing .
Select the OK button.
To avoid having to select the browser profile for each iteration of testing with an
app, set the profile as the default with the Set as Default button.
Make sure that the browser is closed by the IDE for any change to the app, test
user, or provider configuration.

App upgrades
A functioning app may fail immediately after upgrading either the .NET Core SDK on the
development machine or changing package versions within the app. In some cases,
incoherent packages may break an app when performing major upgrades. Most of these
issues can be fixed by following these instructions:
1. Clear the local system's NuGet package caches by executing dotnet nuget locals all
--clear from a command shell.
2. Delete the project's bin and obj folders.
3. Restore and rebuild the project.
4. Delete all of the files in the deployment folder on the server prior to redeploying
the app.

7 Note

Use of package versions incompatible with the app's target framework isn't
supported. For information on a package, use the NuGet Gallery or FuGet
Package Explorer .

Run the Server app


When testing and troubleshooting a hosted Blazor WebAssembly solution, make sure
that you're running the app from the Server project.

Inspect the user


The ASP.NET Core framework's test assets include a Blazor WebAssembly client app
with a User component that can be useful in troubleshooting. The User component can
be used directly in apps or serve as the basis for further customization:

User test component in the dotnet/aspnetcore GitHub repository

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Inspect the content of a JSON Web Token (JWT)


To decode a JSON Web Token (JWT), use Microsoft's jwt.ms tool. Values in the UI
never leave your browser.

Example encoded JWT (shortened for display):


eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0
djhkbE5QNC1j ...
bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzp
D-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-
yBMKV2_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

JSON

{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1610059429,
"nbf": 1610055829,
"ver": "1.0",
"iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-
226dcc9ad298/v2.0/",
"sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
"aud": "70bde375-fce3-4b82-984a-b247d823a03f",
"nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
"iat": 1610055829,
"auth_time": 1610055822,
"idp": "idp.com",
"tfp": "B2C_1_signupsignin"
}.[Signature]

Additional resources
ASP.NET Core Blazor WebAssembly additional security scenarios
Build a custom version of the Authentication.MSAL JavaScript library
Unauthenticated or unauthorized web API requests in an app with a secure default
client
ASP.NET Core Blazor WebAssembly with Azure Active Directory groups and roles
Quickstart: Register an application with the Microsoft identity platform
Quickstart: Configure an application to expose web APIs
Secure an ASP.NET Core Blazor
WebAssembly standalone app with
Azure Active Directory
Article • 03/10/2023

This article explains how to create a standalone Blazor WebAssembly app that uses
Azure Active Directory (AAD) for authentication.

For additional security scenario coverage after reading this article, see ASP.NET Core
Blazor WebAssembly additional security scenarios.

Walkthrough
The subsections of the walkthrough explain how to:

Create a tenant in Azure


Register an app in Azure
Create the Blazor app
Run the app

Create a tenant in Azure


Follow the guidance in Quickstart: Set up a tenant to create a tenant in AAD.

Register an app in Azure


Register an AAD app:

1. Navigate to Azure Active Directory in the Azure portal. Select App registrations in
the sidebar. Select the New registration button.
2. Provide a Name for the app (for example, Blazor Standalone AAD).
3. Choose a Supported account types. You may select Accounts in this
organizational directory only for this experience.
4. Set the Redirect URI dropdown list to Single-page application (SPA) and provide
the following redirect URI: https://localhost/authentication/login-callback . If
you know the production redirect URI for the Azure default host (for example,
azurewebsites.net ) or the custom domain host (for example, contoso.com ), you

can also add the production redirect URI at the same time that you're providing
the localhost redirect URI. Be sure to include the port number for non- :443 ports
in any production redirect URIs that you add.
5. If you're using an unverified publisher domain, clear the Permissions > Grant
admin consent to openid and offline_access permissions checkbox. If the
publisher domain is verified, this checkbox isn't present.
6. Select Register.

7 Note

Supplying the port number for a localhost AAD redirect URI isn't required. For
more information, see Redirect URI (reply URL) restrictions and limitations:
Localhost exceptions (Azure documentation).

Record the following information:

Application (client) ID (for example, 41451fa7-82d9-4673-8fa5-69eff5a761fd )


Directory (tenant) ID (for example, e86c78e2-8bb4-4c41-aefd-918e0565a45e )

In Authentication > Platform configurations > Single-page application:

1. Confirm the redirect URI of https://localhost/authentication/login-callback is


present.
2. In the Implicit grant section, ensure that the checkboxes for Access tokens and ID
tokens aren't selected. Implicit grant isn't recommended for Blazor apps using
MSAL v2.0 or later. For more information, see Secure ASP.NET Core Blazor
WebAssembly.
3. The remaining defaults for the app are acceptable for this experience.
4. Select the Save button if you made changes.

Create the Blazor app


Create the app in an empty folder. Replace the placeholders in the following command
with the information recorded earlier and execute the command in a command shell:

.NET CLI

dotnet new blazorwasm -au SingleOrg --client-id "{CLIENT ID}" -o {PROJECT


NAME} --tenant-id "{TENANT ID}"

Placeholder Azure portal name Example

{PROJECT NAME} — BlazorSample


Placeholder Azure portal name Example

{CLIENT ID} Application (client) ID 41451fa7-82d9-4673-8fa5-69eff5a761fd

{TENANT ID} Directory (tenant) ID e86c78e2-8bb4-4c41-aefd-918e0565a45e

The output location specified with the -o|--output option creates a project folder if it
doesn't exist and becomes part of the project's name.

Add a MsalProviderOptions for User.Read permission with DefaultAccessTokenScopes:

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes
.Add("https://graph.microsoft.com/User.Read");
});

Run the app


Use one of the following approaches to run the app:

Visual Studio
Select the Run button.
Use Debug > Start Debugging from the menu.
Press F5 .
.NET CLI command shell: Execute the dotnet run command from the app's folder.

Parts of the app


This section describes the parts of an app generated from the Blazor WebAssembly
project template and how the app is configured. There's no specific guidance to follow
in this section for a basic working application if you created the app using the guidance
in the Walkthrough section. The guidance in this section is helpful for updating an app
to authenticate and authorize users. However, an alternative approach to updating an
app is to create a new app from the guidance in the Walkthrough section and moving
the app's components, classes, and resources to the new app.

Authentication package
When an app is created to use Work or School Accounts ( SingleOrg ), the app
automatically receives a package reference for the Microsoft Authentication Library
(Microsoft.Authentication.WebAssembly.Msal ). The package provides a set of
primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the


Microsoft.Authentication.WebAssembly.Msal package to the app.

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 .

The Microsoft.Authentication.WebAssembly.Msal package transitively adds the


Microsoft.AspNetCore.Components.WebAssembly.Authentication package to the app.

Authentication service support


Support for authenticating users is registered in the service container with the
AddMsalAuthentication extension method provided by the
Microsoft.Authentication.WebAssembly.Msal package. This method sets up the
services required for the app to interact with the Identity Provider (IP).

Program.cs :

C#

builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
});

The AddMsalAuthentication method accepts a callback to configure the parameters


required to authenticate an app. The values required for configuring the app can be
obtained from the AAD configuration when you register the app.

wwwroot/appsettings.json configuration

Configuration is supplied by the wwwroot/appsettings.json file:


JSON

{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/{TENANT ID}",
"ClientId": "{CLIENT ID}",
"ValidateAuthority": true
}
}

Example:

JSON

{
"AzureAd": {
"Authority":
"https://login.microsoftonline.com/e86c78e2-...-918e0565a45e",
"ClientId": "41451fa7-82d9-4673-8fa5-69eff5a761fd",
"ValidateAuthority": true
}
}

Access token scopes


The Blazor WebAssembly template doesn't automatically configure the app to request
an access token for a secure API. To provision an access token as part of the sign-in flow,
add the scope to the default access token scopes of the MsalProviderOptions:

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

Specify additional scopes with AdditionalScopesToConsent :

C#

options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");

For more information, see the following resources:


Request additional access tokens
Attach tokens to outgoing requests
Quickstart: Configure an application to expose web APIs
Access token scopes for Microsoft Graph API

Login mode
The framework defaults to pop-up login mode and falls back to redirect login mode if a
pop-up can't be opened. Configure MSAL to use redirect login mode by setting the
LoginMode property of MsalProviderOptions to redirect :

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.LoginMode = "redirect";
});

The default setting is popup , and the string value isn't case-sensitive.

Imports file
The Microsoft.AspNetCore.Components.Authorization namespace is made available
throughout the app via the _Imports.razor file:

razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index page
The Index page ( wwwroot/index.html ) page includes a script that defines the
AuthenticationService in JavaScript. AuthenticationService handles the low-level
details of the OIDC protocol. The app internally calls methods defined in the script to
perform the authentication operations.

HTML

<script
src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationServic
e.js"></script>

App component
The App component ( App.razor ) is similar to the App component found in Blazor Server
apps:

The CascadingAuthenticationState component manages exposing the


AuthenticationState to the rest of the app.
The AuthorizeRouteView component makes sure that the current user is
authorized to access a given page or otherwise renders the RedirectToLogin
component.
The RedirectToLogin component manages redirecting unauthorized users to the
login page.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
App component ( App.razor ) isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the App component ( App.razor ) in the generated app.

Inspect the App component ( App.razor ) in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

RedirectToLogin component
The RedirectToLogin component ( Shared/RedirectToLogin.razor ):

Manages redirecting unauthorized users to the login page.


The current URL that the user is attempting to access is maintained by so that they
can be returned to that page if authentication is successful using:
Navigation history state in ASP.NET Core 7.0 or later.
A query string in ASP.NET Core 6.0 or earlier.

Inspect the RedirectToLogin component in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

LoginDisplay component
The LoginDisplay component ( Shared/LoginDisplay.razor ) is rendered in the
MainLayout component ( Shared/MainLayout.razor ) and manages the following

behaviors:

For authenticated users:


Displays the current user name.
Offers a link to the user profile page in ASP.NET Core Identity.
Offers a button to log out of the app.
For anonymous users:
Offers the option to register.
Offers the option to log in.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
LoginDisplay component isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the LoginDisplay component in the generated app.
Inspect the LoginDisplay component in reference source . The templated
content for Hosted equal to true is used.

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

Authentication component
The page produced by the Authentication component ( Pages/Authentication.razor )
defines the routes required for handling different authentication stages.

The RemoteAuthenticatorView component:

Is provided by the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package.
Manages performing the appropriate actions at each stage of authentication.

razor

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
[Parameter]
public string? Action { get; set; }
}

7 Note

Nullable reference types (NRTs) and .NET compiler null-state static analysis is
supported in ASP.NET Core 6.0 or later. Prior to the release of ASP.NET Core 6.0, the
string type appears without the null type designation ( ? ).

Troubleshoot
Logging
This section applies to ASP.NET Core 7.0 or later.

To enable debug or trace logging for Blazor WebAssembly authentication, see ASP.NET
Core Blazor logging.

Common errors
Misconfiguration of the app or Identity Provider (IP)

The most common errors are caused by incorrect configuration. The following are
a few examples:
Depending on the requirements of the scenario, a missing or incorrect
Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI
prevents an app from authenticating clients.
An incorrect access token scope prevents clients from accessing server web API
endpoints.
Incorrect or missing server API permissions prevent clients from accessing
server web API endpoints.
Running the app at a different port than is configured in the Redirect URI of the
Identity Provider's app registration.

Configuration sections of this article's guidance show examples of the correct


configuration. Carefully check each section of the article looking for app and IP
misconfiguration.

If the configuration appears correct:

Analyze application logs.

Examine the network traffic between the client app and the IP or server app with
the browser's developer tools. Often, an exact error message or a message with
a clue to what's causing the problem is returned to the client by the IP or server
app after making a request. Developer tools guidance is found in the following
articles:
Google Chrome (Google documentation)
Microsoft Edge
Mozilla Firefox (Mozilla documentation)

Decode the contents of a JSON Web Token (JWT) used for authenticating a
client or accessing a server web API, depending on where the problem is
occurring. For more information, see Inspect the content of a JSON Web Token
(JWT).

The documentation team responds to document feedback and bugs in articles


(open an issue from the This page feedback section) but is unable to provide
product support. Several public support forums are available to assist with
troubleshooting an app. We recommend the following:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

The preceding forums are not owned or controlled by Microsoft.

For non-security, non-sensitive, and non-confidential reproducible framework bug


reports, open an issue with the ASP.NET Core product unit . Don't open an issue
with the product unit until you've thoroughly investigated the cause of a problem
and can't resolve it on your own and with the help of the community on a public
support forum. The product unit isn't able to troubleshoot individual apps that are
broken due to simple misconfiguration or use cases involving third-party services.
If a report is sensitive or confidential in nature or describes a potential security flaw
in the product that attackers may exploit, see Reporting security issues and bugs
(dotnet/aspnetcore GitHub repository) .

Unauthorized client for AAD

info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

Login callback error from AAD:


Error: unauthorized_client
Description: AADB2C90058: The provided application is not configured to
allow public clients.

To resolve the error:

1. In the Azure portal, access the app's manifest.


2. Set the allowPublicClient attribute to null or true .

Cookies and site data


Cookies and site data can persist across app updates and interfere with testing and
troubleshooting. Clear the following when making app code changes, user account
changes with the provider, or provider app configuration changes:

User sign-in cookies


App cookies
Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing
and troubleshooting is to:

Configure a browser
Use a browser for testing that you can configure to delete all cookie and site
data each time the browser is closed.
Make sure that the browser is closed manually or by the IDE for any change to
the app, test user, or provider configuration.
Use a custom command to open a browser in incognito or private mode in Visual
Studio:
Open Browse With dialog box from Visual Studio's Run button.
Select the Add button.
Provide the path to your browser in the Program field. The following executable
paths are typical installation locations for Windows 10. If your browser is
installed in a different location or you aren't using Windows 10, provide the
path to the browser's executable.
Microsoft Edge: C:\Program Files
(x86)\Microsoft\Edge\Application\msedge.exe

Google Chrome: C:\Program Files


(x86)\Google\Chrome\Application\chrome.exe

Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe


In the Arguments field, provide the command-line option that the browser uses
to open in incognito or private mode. Some browsers require the URL of the
app.
Microsoft Edge: Use -inprivate .
Google Chrome: Use --incognito --new-window {URL} , where the placeholder
{URL} is the URL to open (for example, https://localhost:5001 ).

Mozilla Firefox: Use -private -url {URL} , where the placeholder {URL} is the
URL to open (for example, https://localhost:5001 ).
Provide a name in the Friendly name field. For example, Firefox Auth Testing .
Select the OK button.
To avoid having to select the browser profile for each iteration of testing with an
app, set the profile as the default with the Set as Default button.
Make sure that the browser is closed by the IDE for any change to the app, test
user, or provider configuration.

App upgrades
A functioning app may fail immediately after upgrading either the .NET Core SDK on the
development machine or changing package versions within the app. In some cases,
incoherent packages may break an app when performing major upgrades. Most of these
issues can be fixed by following these instructions:

1. Clear the local system's NuGet package caches by executing dotnet nuget locals all
--clear from a command shell.
2. Delete the project's bin and obj folders.
3. Restore and rebuild the project.
4. Delete all of the files in the deployment folder on the server prior to redeploying
the app.

7 Note

Use of package versions incompatible with the app's target framework isn't
supported. For information on a package, use the NuGet Gallery or FuGet
Package Explorer .

Run the Server app


When testing and troubleshooting a hosted Blazor WebAssembly solution, make sure
that you're running the app from the Server project.

Inspect the user


The ASP.NET Core framework's test assets include a Blazor WebAssembly client app
with a User component that can be useful in troubleshooting. The User component can
be used directly in apps or serve as the basis for further customization:

User test component in the dotnet/aspnetcore GitHub repository

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Inspect the content of a JSON Web Token (JWT)


To decode a JSON Web Token (JWT), use Microsoft's jwt.ms tool. Values in the UI
never leave your browser.

Example encoded JWT (shortened for display):

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0
djhkbE5QNC1j ...
bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzp
D-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-
yBMKV2_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

JSON

{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1610059429,
"nbf": 1610055829,
"ver": "1.0",
"iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-
226dcc9ad298/v2.0/",
"sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
"aud": "70bde375-fce3-4b82-984a-b247d823a03f",
"nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
"iat": 1610055829,
"auth_time": 1610055822,
"idp": "idp.com",
"tfp": "B2C_1_signupsignin"
}.[Signature]

Additional resources
ASP.NET Core Blazor WebAssembly additional security scenarios
Build a custom version of the Authentication.MSAL JavaScript library
Unauthenticated or unauthorized web API requests in an app with a secure default
client
ASP.NET Core Blazor WebAssembly with Azure Active Directory groups and roles
Microsoft identity platform and Azure Active Directory with ASP.NET Core
Microsoft identity platform documentation
Security best practices for application properties in Azure Active Directory
Secure an ASP.NET Core Blazor
WebAssembly standalone app with
Azure Active Directory B2C
Article • 03/10/2023

This article explains how to create a standalone Blazor WebAssembly app that uses
Azure Active Directory (AAD) B2C for authentication.

For additional security scenario coverage after reading this article, see ASP.NET Core
Blazor WebAssembly additional security scenarios.

Walkthrough
The subsections of the walkthrough explain how to:

Create a tenant in Azure


Register an app in Azure
Create the Blazor app
Run the app

Create a tenant in Azure


Follow the guidance in Tutorial: Create an Azure Active Directory B2C tenant to create
an AAD B2C tenant.

Before proceeding with this article's guidance, confirm that you've selected the correct
directory for the AAD B2C tenant.

Register an app in Azure


Register an AAD B2C app:

1. Navigate to Azure AD B2C in the Azure portal. Select App registrations in the
sidebar. Select the New registration button.
2. Provide a Name for the app (for example, Blazor Standalone AAD B2C).
3. For Supported account types, select the multi-tenant option: Accounts in any
organizational directory or any identity provider. For authenticating users with
Azure AD B2C.
4. Set the Redirect URI dropdown list to Single-page application (SPA) and provide
the following redirect URI: https://localhost/authentication/login-callback . If
you know the production redirect URI for the Azure default host (for example,
azurewebsites.net ) or the custom domain host (for example, contoso.com ), you
can also add the production redirect URI at the same time that you're providing
the localhost redirect URI. Be sure to include the port number for non- :443 ports
in any production redirect URIs that you add.
5. If you're using an unverified publisher domain, confirm that Permissions > Grant
admin consent to openid and offline_access permissions is selected. If the
publisher domain is verified, this checkbox isn't present.
6. Select Register.

7 Note

Supplying the port number for a localhost AAD B2C redirect URI isn't required. For
more information, see Redirect URI (reply URL) restrictions and limitations:
Localhost exceptions (Azure documentation).

Record the following information:

Application (client) ID (for example, 41451fa7-82d9-4673-8fa5-69eff5a761fd ).


AAD B2C instance (for example, https://contoso.b2clogin.com/ , which includes
the trailing slash): The instance is the scheme and host of an Azure B2C app
registration, which can be found by opening the Endpoints window from the App
registrations page in the Azure portal.
AAD B2C Primary/Publisher/Tenant domain (for example,
contoso.onmicrosoft.com ): The domain is available as the Publisher domain in the

Branding blade of the Azure portal for the registered app.

In Authentication > Platform configurations > Single-page application:

1. Confirm the redirect URI of https://localhost/authentication/login-callback is


present.
2. In the Implicit grant section, ensure that the checkboxes for Access tokens and ID
tokens aren't selected. Implicit grant isn't recommended for Blazor apps using
MSAL v2.0 or later. For more information, see Secure ASP.NET Core Blazor
WebAssembly.
3. The remaining defaults for the app are acceptable for this experience.
4. Select the Save button if you made changes.

In Home > Azure AD B2C > User flows:


Create a sign-up and sign-in user flow

At a minimum, select the Application claims > Display Name user attribute to populate
the context.User.Identity?.Name / context.User.Identity.Name in the LoginDisplay
component ( Shared/LoginDisplay.razor ).

Record the sign-up and sign-in user flow name created for the app (for example,
B2C_1_signupsignin ).

Create the Blazor app


In an empty folder, replace the placeholders in the following command with the
information recorded earlier and execute the command in a command shell:

.NET CLI

dotnet new blazorwasm -au IndividualB2C --aad-b2c-instance "{AAD B2C


INSTANCE}" --client-id "{CLIENT ID}" --domain "{TENANT DOMAIN}" -o {PROJECT
NAME} -ssp "{SIGN UP OR SIGN IN POLICY}"

Placeholder Azure portal name Example

{AAD B2C INSTANCE} Instance https://contoso.b2clogin.com/ (includes the


trailing slash)

{PROJECT NAME} — BlazorSample

{CLIENT ID} Application (client) ID 41451fa7-82d9-4673-8fa5-69eff5a761fd

{SIGN UP OR SIGN IN Sign-up/sign-in user flow B2C_1_signupsignin1


POLICY}

{TENANT DOMAIN} Primary/Publisher/Tenant contoso.onmicrosoft.com


domain

The output location specified with the -o|--output option creates a project folder if it
doesn't exist and becomes part of the project's name.

Add a pair of MsalProviderOptions for openid and offline_access


DefaultAccessTokenScopes:

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});

After creating the app, you should be able to:

Log into the app using an AAD user account.


Request access tokens for Microsoft APIs. For more information, see:
Access token scopes
Quickstart: Configure an application to expose web APIs.

Run the app


Use one of the following approaches to run the app:

Visual Studio
Select the Run button.
Use Debug > Start Debugging from the menu.
Press F5 .
.NET CLI command shell: Execute the dotnet run command from the app's folder.

Parts of the app


This section describes the parts of an app generated from the Blazor WebAssembly
project template and how the app is configured. There's no specific guidance to follow
in this section for a basic working application if you created the app using the guidance
in the Walkthrough section. The guidance in this section is helpful for updating an app
to authenticate and authorize users. However, an alternative approach to updating an
app is to create a new app from the guidance in the Walkthrough section and moving
the app's components, classes, and resources to the new app.

Authentication package
When an app is created to use an Individual B2C Account ( IndividualB2C ), the app
automatically receives a package reference for the Microsoft Authentication Library
(Microsoft.Authentication.WebAssembly.Msal ). The package provides a set of
primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the


Microsoft.Authentication.WebAssembly.Msal package to the app.
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 .

The Microsoft.Authentication.WebAssembly.Msal package transitively adds the


Microsoft.AspNetCore.Components.WebAssembly.Authentication package to the app.

Authentication service support


Support for authenticating users is registered in the service container with the
AddMsalAuthentication extension method provided by the
Microsoft.Authentication.WebAssembly.Msal package. This method sets up all of the
services required for the app to interact with the Identity Provider (IP).

Program.cs :

C#

builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C",
options.ProviderOptions.Authentication);
});

The AddMsalAuthentication method accepts a callback to configure the parameters


required to authenticate an app. The values required for configuring the app can be
obtained from the AAD configuration when you register the app.

Configuration is supplied by the wwwroot/appsettings.json file:

JSON

{
"AzureAdB2C": {
"Authority": "{AAD B2C INSTANCE}{TENANT DOMAIN}/{SIGN UP OR SIGN IN
POLICY}",
"ClientId": "{CLIENT ID}",
"ValidateAuthority": false
}
}

In the preceding configuration, the {AAD B2C INSTANCE} includes a trailing slash.
Example:

JSON

{
"AzureAdB2C": {
"Authority":
"https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_signupsignin1",
"ClientId": "41451fa7-82d9-4673-8fa5-69eff5a761fd",
"ValidateAuthority": false
}
}

Access token scopes


The Blazor WebAssembly template doesn't automatically configure the app to request
an access token for a secure API. To provision an access token as part of the sign-in flow,
add the scope to the default access token scopes of the MsalProviderOptions:

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

Specify additional scopes with AdditionalScopesToConsent :

C#

options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");

For more information, see the following sections of the Additional scenarios article:

Request additional access tokens


Attach tokens to outgoing requests

Login mode
The framework defaults to pop-up login mode and falls back to redirect login mode if a
pop-up can't be opened. Configure MSAL to use redirect login mode by setting the
LoginMode property of MsalProviderOptions to redirect :
C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.LoginMode = "redirect";
});

The default setting is popup , and the string value isn't case-sensitive.

Imports file
The Microsoft.AspNetCore.Components.Authorization namespace is made available
throughout the app via the _Imports.razor file:

razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index page
The Index page ( wwwroot/index.html ) page includes a script that defines the
AuthenticationService in JavaScript. AuthenticationService handles the low-level

details of the OIDC protocol. The app internally calls methods defined in the script to
perform the authentication operations.

HTML

<script
src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationServic
e.js"></script>

App component
The App component ( App.razor ) is similar to the App component found in Blazor Server
apps:

The CascadingAuthenticationState component manages exposing the


AuthenticationState to the rest of the app.
The AuthorizeRouteView component makes sure that the current user is
authorized to access a given page or otherwise renders the RedirectToLogin
component.
The RedirectToLogin component manages redirecting unauthorized users to the
login page.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
App component ( App.razor ) isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the App component ( App.razor ) in the generated app.

Inspect the App component ( App.razor ) in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

RedirectToLogin component
The RedirectToLogin component ( Shared/RedirectToLogin.razor ):

Manages redirecting unauthorized users to the login page.


The current URL that the user is attempting to access is maintained by so that they
can be returned to that page if authentication is successful using:
Navigation history state in ASP.NET Core 7.0 or later.
A query string in ASP.NET Core 6.0 or earlier.

Inspect the RedirectToLogin component in reference source .


7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

LoginDisplay component
The LoginDisplay component ( Shared/LoginDisplay.razor ) is rendered in the
MainLayout component ( Shared/MainLayout.razor ) and manages the following

behaviors:

For authenticated users:


Displays the current user name.
Offers a link to the user profile page in ASP.NET Core Identity.
Offers a button to log out of the app.
For anonymous users:
Offers the option to register.
Offers the option to log in.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
LoginDisplay component isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the LoginDisplay component in the generated app.

Inspect the LoginDisplay component in reference source . The templated


content for Hosted equal to true is used.

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .
Authentication component
The page produced by the Authentication component ( Pages/Authentication.razor )
defines the routes required for handling different authentication stages.

The RemoteAuthenticatorView component:

Is provided by the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package.
Manages performing the appropriate actions at each stage of authentication.

razor

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
[Parameter]
public string? Action { get; set; }
}

7 Note

Nullable reference types (NRTs) and .NET compiler null-state static analysis is
supported in ASP.NET Core 6.0 or later. Prior to the release of ASP.NET Core 6.0, the
string type appears without the null type designation ( ? ).

Custom policies
The Microsoft Authentication Library (Microsoft.Authentication.WebAssembly.Msal,
NuGet package ) doesn't support AAD B2C custom policies by default.

Troubleshoot

Logging
This section applies to ASP.NET Core 7.0 or later.

To enable debug or trace logging for Blazor WebAssembly authentication, see ASP.NET
Core Blazor logging.
Common errors
Misconfiguration of the app or Identity Provider (IP)

The most common errors are caused by incorrect configuration. The following are
a few examples:
Depending on the requirements of the scenario, a missing or incorrect
Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI
prevents an app from authenticating clients.
An incorrect access token scope prevents clients from accessing server web API
endpoints.
Incorrect or missing server API permissions prevent clients from accessing
server web API endpoints.
Running the app at a different port than is configured in the Redirect URI of the
Identity Provider's app registration.

Configuration sections of this article's guidance show examples of the correct


configuration. Carefully check each section of the article looking for app and IP
misconfiguration.

If the configuration appears correct:

Analyze application logs.

Examine the network traffic between the client app and the IP or server app with
the browser's developer tools. Often, an exact error message or a message with
a clue to what's causing the problem is returned to the client by the IP or server
app after making a request. Developer tools guidance is found in the following
articles:
Google Chrome (Google documentation)
Microsoft Edge
Mozilla Firefox (Mozilla documentation)

Decode the contents of a JSON Web Token (JWT) used for authenticating a
client or accessing a server web API, depending on where the problem is
occurring. For more information, see Inspect the content of a JSON Web Token
(JWT).

The documentation team responds to document feedback and bugs in articles


(open an issue from the This page feedback section) but is unable to provide
product support. Several public support forums are available to assist with
troubleshooting an app. We recommend the following:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

The preceding forums are not owned or controlled by Microsoft.

For non-security, non-sensitive, and non-confidential reproducible framework bug


reports, open an issue with the ASP.NET Core product unit . Don't open an issue
with the product unit until you've thoroughly investigated the cause of a problem
and can't resolve it on your own and with the help of the community on a public
support forum. The product unit isn't able to troubleshoot individual apps that are
broken due to simple misconfiguration or use cases involving third-party services.
If a report is sensitive or confidential in nature or describes a potential security flaw
in the product that attackers may exploit, see Reporting security issues and bugs
(dotnet/aspnetcore GitHub repository) .

Unauthorized client for AAD

info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

Login callback error from AAD:


Error: unauthorized_client
Description: AADB2C90058: The provided application is not configured to
allow public clients.

To resolve the error:

1. In the Azure portal, access the app's manifest.


2. Set the allowPublicClient attribute to null or true .

Cookies and site data


Cookies and site data can persist across app updates and interfere with testing and
troubleshooting. Clear the following when making app code changes, user account
changes with the provider, or provider app configuration changes:

User sign-in cookies


App cookies
Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing
and troubleshooting is to:
Configure a browser
Use a browser for testing that you can configure to delete all cookie and site
data each time the browser is closed.
Make sure that the browser is closed manually or by the IDE for any change to
the app, test user, or provider configuration.
Use a custom command to open a browser in incognito or private mode in Visual
Studio:
Open Browse With dialog box from Visual Studio's Run button.
Select the Add button.
Provide the path to your browser in the Program field. The following executable
paths are typical installation locations for Windows 10. If your browser is
installed in a different location or you aren't using Windows 10, provide the
path to the browser's executable.
Microsoft Edge: C:\Program Files
(x86)\Microsoft\Edge\Application\msedge.exe

Google Chrome: C:\Program Files


(x86)\Google\Chrome\Application\chrome.exe
Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe
In the Arguments field, provide the command-line option that the browser uses
to open in incognito or private mode. Some browsers require the URL of the
app.
Microsoft Edge: Use -inprivate .
Google Chrome: Use --incognito --new-window {URL} , where the placeholder
{URL} is the URL to open (for example, https://localhost:5001 ).
Mozilla Firefox: Use -private -url {URL} , where the placeholder {URL} is the
URL to open (for example, https://localhost:5001 ).
Provide a name in the Friendly name field. For example, Firefox Auth Testing .
Select the OK button.
To avoid having to select the browser profile for each iteration of testing with an
app, set the profile as the default with the Set as Default button.
Make sure that the browser is closed by the IDE for any change to the app, test
user, or provider configuration.

App upgrades
A functioning app may fail immediately after upgrading either the .NET Core SDK on the
development machine or changing package versions within the app. In some cases,
incoherent packages may break an app when performing major upgrades. Most of these
issues can be fixed by following these instructions:
1. Clear the local system's NuGet package caches by executing dotnet nuget locals all
--clear from a command shell.
2. Delete the project's bin and obj folders.
3. Restore and rebuild the project.
4. Delete all of the files in the deployment folder on the server prior to redeploying
the app.

7 Note

Use of package versions incompatible with the app's target framework isn't
supported. For information on a package, use the NuGet Gallery or FuGet
Package Explorer .

Run the Server app


When testing and troubleshooting a hosted Blazor WebAssembly solution, make sure
that you're running the app from the Server project.

Inspect the user


The ASP.NET Core framework's test assets include a Blazor WebAssembly client app
with a User component that can be useful in troubleshooting. The User component can
be used directly in apps or serve as the basis for further customization:

User test component in the dotnet/aspnetcore GitHub repository

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Inspect the content of a JSON Web Token (JWT)


To decode a JSON Web Token (JWT), use Microsoft's jwt.ms tool. Values in the UI
never leave your browser.

Example encoded JWT (shortened for display):


eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0
djhkbE5QNC1j ...
bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzp
D-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-
yBMKV2_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

JSON

{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1610059429,
"nbf": 1610055829,
"ver": "1.0",
"iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-
226dcc9ad298/v2.0/",
"sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
"aud": "70bde375-fce3-4b82-984a-b247d823a03f",
"nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
"iat": 1610055829,
"auth_time": 1610055822,
"idp": "idp.com",
"tfp": "B2C_1_signupsignin"
}.[Signature]

Additional resources
ASP.NET Core Blazor WebAssembly additional security scenarios
Build a custom version of the Authentication.MSAL JavaScript library
Unauthenticated or unauthorized web API requests in an app with a secure default
client
Cloud authentication with Azure Active Directory B2C in ASP.NET Core
Tutorial: Create an Azure Active Directory B2C tenant
Tutorial: Register an application in Azure Active Directory B2C
Microsoft identity platform documentation
Secure a hosted ASP.NET Core Blazor
WebAssembly app with Azure Active
Directory
Article • 04/25/2023

This article explains how to create a hosted Blazor WebAssembly solution that uses
Azure Active Directory (AAD) for authentication. This article focuses on a single tenant
app with a single tenant Azure app registration.

This article doesn't cover a multi-tenant AAD registration. For more information, see
Making your application multi-tenant.

This article focuses on the use of an Azure Active Directory tenant, as described in
Quickstart: Set up a tenant. If the app is registered in an Azure Active Directory B2C
tenant, as described in Tutorial: Create an Azure Active Directory B2C tenant but follows
the guidance in this article, the App ID URI is managed differently by AAD. For more
information, see the Use of an Azure Active Directory B2C tenant section of this article.

For additional security scenario coverage after reading this article, see ASP.NET Core
Blazor WebAssembly additional security scenarios.

Walkthrough
The subsections of the walkthrough explain how to:

Create a tenant in Azure


Register a server API app in Azure
Register a client app in Azure
Create the Blazor app
Modify the Server appsettings.json configuration
Modify the default access token scope scheme
Run the app

Create a tenant in Azure


Follow the guidance in Quickstart: Set up a tenant to create a tenant in AAD.

Register a server API app in Azure


Register an AAD app for the Server API app:

1. Navigate to Azure Active Directory in the Azure portal. Select App registrations in
the sidebar. Select the New registration button.
2. Provide a Name for the app (for example, Blazor Server AAD).
3. Choose a Supported account types. You may select Accounts in this
organizational directory only (single tenant) for this experience.
4. The Server API app doesn't require a Redirect URI in this scenario, so leave the
Select a platform dropdown list unselected and don't enter a redirect URI.
5. This article assumes the app is registered in an Azure Active Directory tenant. If
the app is registered in an Azure Active Directory B2C tenant, the Permissions >
Grant admin consent to openid and offline_access permissions checkbox is
present and selected. Deselect the checkbox to disable the setting. When using an
Active Azure Directory tenant, the checkbox isn't present.
6. Select Register.

Record the following information:

Server API app Application (client) ID (for example, 41451fa7-82d9-4673-8fa5-


69eff5a761fd )
Directory (tenant) ID (for example, e86c78e2-8bb4-4c41-aefd-918e0565a45e )
AAD Primary/Publisher/Tenant domain (for example, contoso.onmicrosoft.com ):
The domain is available as the Publisher domain in the Branding blade of the
Azure portal for the registered app.

In API permissions, remove the Microsoft Graph > User.Read permission, as the server
API app doesn't require additional API access for merely signing in users and calling
server API endpoints.

In Expose an API:

1. Confirm or add the App ID URI in the format api://{SERVER API APP CLIENT ID} .
2. Select Add a scope.
3. Select Save and continue.
4. Provide a Scope name (for example, API.Access ).
5. Provide an Admin consent display name (for example, Access API ).
6. Provide an Admin consent description (for example, Allows the app to access
server app API endpoints. ).

7. Confirm that the State is set to Enabled.


8. Select Add scope.

Record the following information:


App ID URI GUID (for example, record 41451fa7-82d9-4673-8fa5-69eff5a761fd from
the App ID URI of api://41451fa7-82d9-4673-8fa5-69eff5a761fd )
Scope name (for example, API.Access )

) Important

If a custom value is used for the App ID URI, configuration changes are required to
both the Server and Client apps after the apps are created from the Blazor
WebAssembly project template. For more information, see the Use of a custom
App ID URI section.

Register a client app in Azure


Register an AAD app for the Client app:

1. Navigate to Azure Active Directory in the Azure portal. Select App registrations in
the sidebar. Select the New registration button.
2. Provide a Name for the app (for example, Blazor Client AAD).
3. Choose a Supported account types. You may select Accounts in this
organizational directory only (single tenant) for this experience.
4. Set the Redirect URI dropdown list to Single-page application (SPA) and provide
the following redirect URI: https://localhost/authentication/login-callback . If
you know the production redirect URI for the Azure default host (for example,
azurewebsites.net ) or the custom domain host (for example, contoso.com ), you

can also add the production redirect URI at the same time that you're providing
the localhost redirect URI. Be sure to include the port number for non- :443 ports
in any production redirect URIs that you add.
5. This article assumes the app is registered in an Azure Active Directory tenant. If
the app is registered in an Azure Active Directory B2C tenant, the Permissions >
Grant admin consent to openid and offline_access permissions checkbox is
present and selected. Deselect the checkbox to disable the setting. When using an
Active Azure Directory tenant, the checkbox isn't present.
6. Select Register.

7 Note

Supplying the port number for a localhost AAD redirect URI isn't required. For
more information, see Redirect URI (reply URL) restrictions and limitations:
Localhost exceptions (Azure documentation).
Record the Client app Application (client) ID (for example, 4369008b-21fa-427c-abaa-
9b53bf58e538 ).

In Authentication > Platform configurations > Single-page application:

1. Confirm the redirect URI of https://localhost/authentication/login-callback is


present.
2. In the Implicit grant section, ensure that the checkboxes for Access tokens and ID
tokens aren't selected. Implicit grant isn't recommended for Blazor apps using
MSAL v2.0 or later. For more information, see Secure ASP.NET Core Blazor
WebAssembly.
3. The remaining defaults for the app are acceptable for this experience.
4. Select the Save button if you made changes.

In API permissions:

1. Confirm that the app has Microsoft Graph > User.Read permission.
2. Select Add a permission followed by My APIs.
3. Select the Server API app from the Name column (for example, Blazor Server
AAD).
4. Open the API list.
5. Enable access to the API (for example, API.Access ).
6. Select Add permissions.
7. Select the Grant admin consent for {TENANT NAME} button. Select Yes to
confirm.

) Important

If you don't have the authority to grant admin consent to the tenant in the last step
of API permissions configuration because consent to use the app is delegated to
users, then you must take the following additional steps:

The app must use a trusted publisher domain.


In the Server app's configuration in the Azure portal, select Expose an API.
Under Authorized client applications, select the button to Add a client
application. Add the Client app's Application (client) ID (for example,
4369008b-21fa-427c-abaa-9b53bf58e538 ).

Create the Blazor app


In an empty folder, replace the placeholders in the following command with the
information recorded earlier and execute the command in a command shell:

.NET CLI

dotnet new blazorwasm -au SingleOrg --api-client-id "{SERVER API APP CLIENT
ID}" --app-id-uri "{SERVER API APP ID URI GUID}" --client-id "{CLIENT APP
CLIENT ID}" --default-scope "{DEFAULT SCOPE}" --domain "{TENANT DOMAIN}" -ho
-o {PROJECT NAME} --tenant-id "{TENANT ID}"

2 Warning

Avoid using dashes ( - ) in the app name {PROJECT NAME} that break the formation
of the OIDC app identifier. Logic in the Blazor WebAssembly project template uses
the project name for an OIDC app identifier in the solution's configuration. Pascal
case ( BlazorSample ) or underscores ( Blazor_Sample ) are acceptable alternatives. For
more information, see Dashes in a hosted Blazor WebAssembly project name
break OIDC security (dotnet/aspnetcore #35337) .

Placeholder Azure portal name Example

{PROJECT — BlazorSample
NAME}

{CLIENT APP Application (client) ID for 4369008b-21fa-427c-abaa-9b53bf58e538


CLIENT ID} the Client app

{DEFAULT Scope name API.Access


SCOPE}

{SERVER API Application (client) ID for 41451fa7-82d9-4673-8fa5-69eff5a761fd


APP CLIENT the Server API app
ID}

{SERVER API Application ID URI GUID 41451fa7-82d9-4673-8fa5-69eff5a761fd (GUID ONLY,


APP ID URI by default matches the {SERVER API APP CLIENT ID} )
GUID}

{TENANT Primary/Publisher/Tenant contoso.onmicrosoft.com


DOMAIN} domain

{TENANT ID} Directory (tenant) ID e86c78e2-8bb4-4c41-aefd-918e0565a45e

The output location specified with the -o|--output option creates a project folder if it
doesn't exist and becomes part of the project's name. Avoid using dashes ( - ) in the
app name that break the formation of the OIDC app identifier (see the earlier
WARNING).

) Important

If a custom value is used for the App ID URI, configuration changes are required to
both the Server and Client apps after the apps are created from the Blazor
WebAssembly project template. For more information, see the Use of a custom
App ID URI section.

Run the app


Run the app from the Server project. When using Visual Studio, either:

Select the dropdown arrow next to the Run button. Open Configure Startup
Projects from the dropdown list. Select the Single startup project option. Confirm
or change the project for the startup project to the Server project.

Confirm that the Server project is highlighted in Solution Explorer before you
start the app with any of the following approaches:
Select the Run button.
Use Debug > Start Debugging from the menu.
Press F5 .

In a command shell, navigate to the Server project folder of the solution. Execute
the dotnet run command.

Configure User.Identity.Name
The guidance in this section covers optionally populating User.Identity.Name with the
value from the name claim.

By default, the Server app API populates User.Identity.Name with the value from the
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name claim type (for example,

2d64b3da-d9d5-42c6-9352-53d8df33d770@contoso.onmicrosoft.com ).

To configure the app to receive the value from the name claim type:

Add a namespace for Microsoft.AspNetCore.Authentication.JwtBearer to


Program.cs :
C#

using Microsoft.AspNetCore.Authentication.JwtBearer;

Configure the TokenValidationParameters.NameClaimType of the


JwtBearerOptions.

C#

builder.Services.Configure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.NameClaimType = "name";
});

Parts of the solution


This section describes the parts of a solution generated from the Blazor WebAssembly
project template and describes how the solution's Client and Server projects are
configured for reference. There's no specific guidance to follow in this section for a basic
working application if you created the app using the guidance in the Walkthrough
section. The guidance in this section is helpful for updating an app to authenticate and
authorize users. However, an alternative approach to updating an app is to create a new
app from the guidance in the Walkthrough section and moving the app's components,
classes, and resources to the new app.

appsettings.json configuration

This section pertains to the solution's Server app.

The appsettings.json file contains the options to configure the JWT bearer handler
used to validate access tokens. Add the following AzureAd configuration section:

JSON

{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "{TENANT DOMAIN}",
"TenantId": "{TENANT ID}",
"ClientId": "{SERVER API APP CLIENT ID}",
"CallbackPath": "/signin-oidc",
"Scopes": "{SCOPES}"
}
}
Example:

JSON

{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "contoso.onmicrosoft.com",
"TenantId": "e86c78e2-8bb4-4c41-aefd-918e0565a45e",
"ClientId": "41451fa7-82d9-4673-8fa5-69eff5a761fd",
"CallbackPath": "/signin-oidc",
"Scopes": "API.Access"
}
}

) Important

If the Server app is registered to use a custom App ID URI in AAD (not in the
default format api://{SERVER API APP CLIENT ID} ), see the Use of a custom App ID
URI section. Changes are required in both the Server and Client apps.

Authentication package
This section pertains to the solution's Server app.

The support for authenticating and authorizing calls to ASP.NET Core web APIs with the
Microsoft Identity Platform is provided by the Microsoft.Identity.Web package.

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 .

The Server app of a hosted Blazor solution created from the Blazor WebAssembly
template includes the Microsoft.Identity.Web.UI package by default. The package
adds UI for user authentication in web apps and isn't used by the Blazor framework. If
the Server app won't be used to authenticate users directly, it's safe to remove the
package reference from the Server app's project file.

Authentication service support


This section pertains to the solution's Server app.

The AddAuthentication method sets up authentication services within the app and
configures the JWT Bearer handler as the default authentication method. The
AddMicrosoftIdentityWebApi method configures services to protect the web API with
Microsoft Identity Platform v2.0. This method expects an AzureAd section in the app's
configuration with the necessary settings to initialize authentication options.

C#

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));

7 Note

When a single authentication scheme is registered, the authentication scheme is


automatically used as the app's default scheme, and it isn't necessary to state the
scheme to AddAuthentication or via AuthenticationOptions. For more
information, see Overview of ASP.NET Core Authentication and the ASP.NET Core
announcement (aspnet/Announcements #490) .

UseAuthentication and UseAuthorization ensure that:

The app attempts to parse and validate tokens on incoming requests.


Any request attempting to access a protected resource without proper credentials
fails.

C#

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

WeatherForecast controller
This section pertains to the solution's Server app.

The WeatherForecast controller ( Controllers/WeatherForecastController.cs ) exposes a


protected API with the [Authorize] attribute applied to the controller. It's important to
understand that:

The [Authorize] attribute in this API controller is the only thing that protects this
API from unauthorized access.
The [Authorize] attribute used in the Blazor WebAssembly app only serves as a hint
to the app that the user should be authorized for the app to work correctly.

C#

[Authorize]
[ApiController]
[Route("[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
...
}
}

wwwroot/appsettings.json configuration

This section pertains to the solution's Client app.

Configuration is supplied by the wwwroot/appsettings.json file:

JSON

{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/{TENANT ID}",
"ClientId": "{CLIENT APP CLIENT ID}",
"ValidateAuthority": true
}
}

Example:

JSON

{
"AzureAd": {
"Authority":
"https://login.microsoftonline.com/e86c78e2-...-918e0565a45e",
"ClientId": "4369008b-21fa-427c-abaa-9b53bf58e538",
"ValidateAuthority": true
}
}
Authentication package
This section pertains to the solution's Client app.

When an app is created to use Work or School Accounts ( SingleOrg ), the app
automatically receives a package reference for the Microsoft Authentication Library
(Microsoft.Authentication.WebAssembly.Msal ). The package provides a set of
primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the


Microsoft.Authentication.WebAssembly.Msal package to the app.

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 .

The Microsoft.Authentication.WebAssembly.Msal package transitively adds the


Microsoft.AspNetCore.Components.WebAssembly.Authentication package to the app.

Authentication service support


This section pertains to the solution's Client app.

Support for HttpClient instances is added that include access tokens when making
requests to the Server app.

Program.cs :

C#

builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI", client =>


client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()


.CreateClient("{PROJECT NAME}.ServerAPI"));

The placeholder {PROJECT NAME} is the project name at solution creation. For example,
providing a project name of BlazorSample produces a named HttpClient of
BlazorSample.ServerAPI .
Support for authenticating users is registered in the service container with the
AddMsalAuthentication extension method provided by the
Microsoft.Authentication.WebAssembly.Msal package. This method sets up the
services required for the app to interact with the Identity Provider (IP).

Program.cs :

C#

builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

The AddMsalAuthentication method accepts a callback to configure the parameters


required to authenticate an app. The values required for configuring the app can be
obtained from the Azure Portal AAD configuration when you register the app.

Access token scopes


This section pertains to the solution's Client app.

The default access token scopes represent the list of access token scopes that are:

Included by default in the sign in request.


Used to provision an access token immediately after authentication.

Additional scopes can be added as needed in Program.cs :

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

Specify additional scopes with AdditionalScopesToConsent :

C#

options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");
Example default access token scope:

C#

options.ProviderOptions.DefaultAccessTokenScopes.Add(
"api://41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access");

For more information, see the following sections of the Additional scenarios article:

Request additional access tokens


Attach tokens to outgoing requests

Login mode
This section pertains to the solution's Client app.

The framework defaults to pop-up login mode and falls back to redirect login mode if a
pop-up can't be opened. Configure MSAL to use redirect login mode by setting the
LoginMode property of MsalProviderOptions to redirect :

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.LoginMode = "redirect";
});

The default setting is popup , and the string value isn't case-sensitive.

Imports file
This section pertains to the solution's Client app.

The Microsoft.AspNetCore.Components.Authorization namespace is made available


throughout the app via the _Imports.razor file:

razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index page
This section pertains to the solution's Client app.

The Index page ( wwwroot/index.html ) page includes a script that defines the
AuthenticationService in JavaScript. AuthenticationService handles the low-level

details of the OIDC protocol. The app internally calls methods defined in the script to
perform the authentication operations.

HTML

<script
src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationServic
e.js"></script>

App component
This section pertains to the solution's Client app.

The App component ( App.razor ) is similar to the App component found in Blazor Server
apps:

The CascadingAuthenticationState component manages exposing the


AuthenticationState to the rest of the app.
The AuthorizeRouteView component makes sure that the current user is
authorized to access a given page or otherwise renders the RedirectToLogin
component.
The RedirectToLogin component manages redirecting unauthorized users to the
login page.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
App component ( App.razor ) isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the App component ( App.razor ) in the generated app.
Inspect the App component ( App.razor ) in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

RedirectToLogin component
This section pertains to the solution's Client app.

The RedirectToLogin component ( Shared/RedirectToLogin.razor ):

Manages redirecting unauthorized users to the login page.


The current URL that the user is attempting to access is maintained by so that they
can be returned to that page if authentication is successful using:
Navigation history state in ASP.NET Core 7.0 or later.
A query string in ASP.NET Core 6.0 or earlier.

Inspect the RedirectToLogin component in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

LoginDisplay component
This section pertains to the solution's Client app.

The LoginDisplay component ( Shared/LoginDisplay.razor ) is rendered in the


MainLayout component ( Shared/MainLayout.razor ) and manages the following

behaviors:

For authenticated users:


Displays the current user name.
Offers a link to the user profile page in ASP.NET Core Identity.
Offers a button to log out of the app.
For anonymous users:
Offers the option to register.
Offers the option to log in.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
LoginDisplay component isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the LoginDisplay component in the generated app.

Inspect the LoginDisplay component in reference source . The templated


content for Hosted equal to true is used.

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

Authentication component
This section pertains to the solution's Client app.

The page produced by the Authentication component ( Pages/Authentication.razor )


defines the routes required for handling different authentication stages.

The RemoteAuthenticatorView component:

Is provided by the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package.
Manages performing the appropriate actions at each stage of authentication.

razor

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code {
[Parameter]
public string? Action { get; set; }
}

7 Note

Nullable reference types (NRTs) and .NET compiler null-state static analysis is
supported in ASP.NET Core 6.0 or later. Prior to the release of ASP.NET Core 6.0, the
string type appears without the null type designation ( ? ).

FetchData component
This section pertains to the solution's Client app.

The FetchData component shows how to:

Provision an access token.


Use the access token to call a protected resource API in the Server app.

The @attribute [Authorize] directive indicates to the Blazor WebAssembly authorization


system that the user must be authorized in order to visit this component. The presence
of the attribute in the Client app doesn't prevent the API on the server from being
called without proper credentials. The Server app also must use [Authorize] on the
appropriate endpoints to correctly protect them.

IAccessTokenProvider.RequestAccessToken takes care of requesting an access token that


can be added to the request to call the API. If the token is cached or the service is able
to provision a new access token without user interaction, the token request succeeds.
Otherwise, the token request fails with an AccessTokenNotAvailableException, which is
caught in a try-catch statement.

In order to obtain the actual token to include in the request, the app must check that
the request succeeded by calling tokenResult.TryGetToken(out var token).

If the request was successful, the token variable is populated with the access token. The
AccessToken.Value property of the token exposes the literal string to include in the
Authorization request header.

If the request failed because the token couldn't be provisioned without user interaction:
ASP.NET Core 7.0 or later: The app navigates to
AccessTokenResult.InteractiveRequestUrl using the given
AccessTokenResult.InteractionOptions to allow refreshing the access token.

ASP.NET Core 6.0 or earlier: The token result contains a redirect URL. Navigating to
this URL takes the user to the login page and back to the current page after a
successful authentication.

razor

@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
private WeatherForecast[] forecasts;

protected override async Task OnInitializedAsync()


{
try
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
("WeatherForecast");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}

Use of an Azure Active Directory B2C tenant


If the app is registered in an Azure Active Directory B2C tenant, as described in Tutorial:
Create an Azure Active Directory B2C tenant but follows the guidance in this article, the
App ID URI is managed differently by AAD.

You can check the tenant type of an existing tenant by selecting the Manage tenants
link at the top of the AAD organization Overview. Examine the Tenant type column
value for the organization. This section pertains to apps that follow the guidance in this
article but that are registered in an Azure Active Directory B2C tenant.
Instead of the App ID URI matching the format api://{SERVER API APP CLIENT ID OR
CUSTOM VALUE} , the App ID URI has the format
https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE} . This

difference affects Client and Server app configurations:

For the server API app, set the Audience in the app settings file ( appsettings.json )
to match the app's audience (App ID URI) provided by the Azure portal with no
trailing slash:

JSON

"Audience": "https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID


OR CUSTOM VALUE}"

Example:

JSON

"Audience": "https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-
69eff5a761fd"

In Program.cs of the Client app, set the audience of the scope (App ID URI) to
match the server API app's audience:

C#

options.ProviderOptions.DefaultAccessTokenScopes
.Add("https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR
CUSTOM VALUE}/{DEFAULT SCOPE}");

In the preceding scope, the App ID URI/audience is the


https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE}

portion of the value, which doesn't include a trailing slash ( / ) and doesn't include
the scope name ( {DEFAULT SCOPE} ).

Example:

C#

options.ProviderOptions.DefaultAccessTokenScopes
.Add("https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-
69eff5a761fd/API.Access");
In the preceding scope, the App ID URI/audience is the
https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd portion
of the value, which doesn't include a trailing slash ( / ) and doesn't include the
scope name ( API.Access ).

Use of a custom App ID URI


If the App ID URI is a custom value, you must manually update the default access token
scope URI in the Client app and add the audience to the Server app's AAD
configuration.

) Important

The following configuration is not required when using the default App ID URI of
api://{SERVER API APP CLIENT ID} .

Example App ID URI of urn://custom-app-id-uri and a scope name of API.Access :

In Program.cs of the Client app:

C#

options.ProviderOptions.DefaultAccessTokenScopes.Add(
"urn://custom-app-id-uri/API.Access");

In appsettings.json of the Server app, add an Audience entry with only the App
ID URI and no trailing slash:

JSON

"Audience": "urn://custom-app-id-uri"

Troubleshoot

Logging
This section applies to ASP.NET Core 7.0 or later.

To enable debug or trace logging for Blazor WebAssembly authentication, see ASP.NET
Core Blazor logging.
Common errors
Misconfiguration of the app or Identity Provider (IP)

The most common errors are caused by incorrect configuration. The following are
a few examples:
Depending on the requirements of the scenario, a missing or incorrect
Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI
prevents an app from authenticating clients.
An incorrect access token scope prevents clients from accessing server web API
endpoints.
Incorrect or missing server API permissions prevent clients from accessing
server web API endpoints.
Running the app at a different port than is configured in the Redirect URI of the
Identity Provider's app registration.

Configuration sections of this article's guidance show examples of the correct


configuration. Carefully check each section of the article looking for app and IP
misconfiguration.

If the configuration appears correct:

Analyze application logs.

Examine the network traffic between the client app and the IP or server app with
the browser's developer tools. Often, an exact error message or a message with
a clue to what's causing the problem is returned to the client by the IP or server
app after making a request. Developer tools guidance is found in the following
articles:
Google Chrome (Google documentation)
Microsoft Edge
Mozilla Firefox (Mozilla documentation)

Decode the contents of a JSON Web Token (JWT) used for authenticating a
client or accessing a server web API, depending on where the problem is
occurring. For more information, see Inspect the content of a JSON Web Token
(JWT).

The documentation team responds to document feedback and bugs in articles


(open an issue from the This page feedback section) but is unable to provide
product support. Several public support forums are available to assist with
troubleshooting an app. We recommend the following:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

The preceding forums are not owned or controlled by Microsoft.

For non-security, non-sensitive, and non-confidential reproducible framework bug


reports, open an issue with the ASP.NET Core product unit . Don't open an issue
with the product unit until you've thoroughly investigated the cause of a problem
and can't resolve it on your own and with the help of the community on a public
support forum. The product unit isn't able to troubleshoot individual apps that are
broken due to simple misconfiguration or use cases involving third-party services.
If a report is sensitive or confidential in nature or describes a potential security flaw
in the product that attackers may exploit, see Reporting security issues and bugs
(dotnet/aspnetcore GitHub repository) .

Unauthorized client for AAD

info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

Login callback error from AAD:


Error: unauthorized_client
Description: AADB2C90058: The provided application is not configured to
allow public clients.

To resolve the error:

1. In the Azure portal, access the app's manifest.


2. Set the allowPublicClient attribute to null or true .

Cookies and site data


Cookies and site data can persist across app updates and interfere with testing and
troubleshooting. Clear the following when making app code changes, user account
changes with the provider, or provider app configuration changes:

User sign-in cookies


App cookies
Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing
and troubleshooting is to:
Configure a browser
Use a browser for testing that you can configure to delete all cookie and site
data each time the browser is closed.
Make sure that the browser is closed manually or by the IDE for any change to
the app, test user, or provider configuration.
Use a custom command to open a browser in incognito or private mode in Visual
Studio:
Open Browse With dialog box from Visual Studio's Run button.
Select the Add button.
Provide the path to your browser in the Program field. The following executable
paths are typical installation locations for Windows 10. If your browser is
installed in a different location or you aren't using Windows 10, provide the
path to the browser's executable.
Microsoft Edge: C:\Program Files
(x86)\Microsoft\Edge\Application\msedge.exe

Google Chrome: C:\Program Files


(x86)\Google\Chrome\Application\chrome.exe
Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe
In the Arguments field, provide the command-line option that the browser uses
to open in incognito or private mode. Some browsers require the URL of the
app.
Microsoft Edge: Use -inprivate .
Google Chrome: Use --incognito --new-window {URL} , where the placeholder
{URL} is the URL to open (for example, https://localhost:5001 ).
Mozilla Firefox: Use -private -url {URL} , where the placeholder {URL} is the
URL to open (for example, https://localhost:5001 ).
Provide a name in the Friendly name field. For example, Firefox Auth Testing .
Select the OK button.
To avoid having to select the browser profile for each iteration of testing with an
app, set the profile as the default with the Set as Default button.
Make sure that the browser is closed by the IDE for any change to the app, test
user, or provider configuration.

App upgrades
A functioning app may fail immediately after upgrading either the .NET Core SDK on the
development machine or changing package versions within the app. In some cases,
incoherent packages may break an app when performing major upgrades. Most of these
issues can be fixed by following these instructions:
1. Clear the local system's NuGet package caches by executing dotnet nuget locals all
--clear from a command shell.
2. Delete the project's bin and obj folders.
3. Restore and rebuild the project.
4. Delete all of the files in the deployment folder on the server prior to redeploying
the app.

7 Note

Use of package versions incompatible with the app's target framework isn't
supported. For information on a package, use the NuGet Gallery or FuGet
Package Explorer .

Run the Server app


When testing and troubleshooting a hosted Blazor WebAssembly solution, make sure
that you're running the app from the Server project.

Inspect the user


The ASP.NET Core framework's test assets include a Blazor WebAssembly client app
with a User component that can be useful in troubleshooting. The User component can
be used directly in apps or serve as the basis for further customization:

User test component in the dotnet/aspnetcore GitHub repository

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Inspect the content of a JSON Web Token (JWT)


To decode a JSON Web Token (JWT), use Microsoft's jwt.ms tool. Values in the UI
never leave your browser.

Example encoded JWT (shortened for display):


eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0
djhkbE5QNC1j ...
bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzp
D-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-
yBMKV2_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

JSON

{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1610059429,
"nbf": 1610055829,
"ver": "1.0",
"iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-
226dcc9ad298/v2.0/",
"sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
"aud": "70bde375-fce3-4b82-984a-b247d823a03f",
"nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
"iat": 1610055829,
"auth_time": 1610055822,
"idp": "idp.com",
"tfp": "B2C_1_signupsignin"
}.[Signature]

Additional resources
Configure an app's publisher domain
Azure Active Directory app manifest: identifierUris attribute
ASP.NET Core Blazor WebAssembly additional security scenarios
Build a custom version of the Authentication.MSAL JavaScript library
Unauthenticated or unauthorized web API requests in an app with a secure default
client
ASP.NET Core Blazor WebAssembly with Azure Active Directory groups and roles
Microsoft identity platform and Azure Active Directory with ASP.NET Core
Microsoft identity platform documentation
Quickstart: Register an application with the Microsoft identity platform
Security best practices for application properties in Azure Active Directory
Secure a hosted ASP.NET Core Blazor
WebAssembly app with Azure Active
Directory B2C
Article • 04/25/2023

This article explains how to create a hosted Blazor WebAssembly solution that uses
Azure Active Directory (AAD) B2C for authentication.

For additional security scenario coverage after reading this article, see ASP.NET Core
Blazor WebAssembly additional security scenarios.

Walkthrough
The subsections of the walkthrough explain how to:

Create a tenant in Azure


Register a server API app in Azure
Register a client app in Azure
Create the Blazor app
Modify the default access token scope scheme
Run the app

Create a tenant in Azure


Follow the guidance in Tutorial: Create an Azure Active Directory B2C tenant to create
an AAD B2C tenant.

Before proceeding with this article's guidance, confirm that you've selected the correct
directory for the AAD B2C tenant.

Register a server API app in Azure


Register an AAD B2C app for the Server API app:

1. Navigate to Azure AD B2C in the Azure portal. Select App registrations in the
sidebar. Select the New registration button.
2. Provide a Name for the app (for example, Blazor Server AAD B2C).
3. For Supported account types, select the multi-tenant option: Accounts in any
identity provider or organizational directory (for authenticating users with user
flows)
4. The Server API app doesn't require a Redirect URI in this scenario, so leave the
Select a platform dropdown list unselected and don't enter a redirect URI.
5. Confirm that Permissions > Grant admin consent to openid and offline_access
permissions is selected.
6. Select Register.

Record the following information:

Server API app Application (client) ID (for example, 41451fa7-82d9-4673-8fa5-


69eff5a761fd )
AAD B2C instance (for example, https://contoso.b2clogin.com/ , which includes
the trailing slash). The instance is the scheme and host of an Azure B2C app
registration, which can be found by opening the Endpoints window from the App
registrations page in the Azure portal.
AAD Primary/Publisher/Tenant domain (for example, contoso.onmicrosoft.com ):
The domain is available as the Publisher domain in the Branding blade of the
Azure portal for the registered app.

Select Expose an API from the sidebar and follow these steps:

1. Select Add a scope.


2. Select Save and continue.
3. Provide a Scope name (for example, API.Access ).
4. Provide an Admin consent display name (for example, Access API ).
5. Provide an Admin consent description (for example, Allows the app to access
server app API endpoints. ).

6. Confirm that the State is set to Enabled.


7. Select Add scope.

Record the following information:

App ID URI GUID (for example, record 41451fa7-82d9-4673-8fa5-69eff5a761fd from


https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd )

Scope name (for example, API.Access )

Register a client app in Azure


Register an AAD B2C app for the Client app:

1. Navigate to Azure AD B2C in the Azure portal. Select App registrations in the
sidebar. Select the New registration button.
2. Provide a Name for the app (for example, Blazor Client AAD B2C).
3. For Supported account types, select the multi-tenant option: Accounts in any
identity provider or organizational directory (for authenticating users with user
flows)
4. Set the Redirect URI dropdown list to Single-page application (SPA) and provide a
redirect URI value of https://localhost/authentication/login-callback . If you
know the production redirect URI for the Azure default host (for example,
azurewebsites.net ) or the custom domain host (for example, contoso.com ), you

can also add the production redirect URI at the same time that you're providing
the localhost redirect URI. Be sure to include the port number for non- :443 ports
in any production redirect URIs that you add.
5. Confirm that Permissions > Grant admin consent to openid and offline_access
permissions is selected.
6. Select Register.

7 Note

Supplying the port number for a localhost AAD B2C redirect URI isn't required. For
more information, see Redirect URI (reply URL) restrictions and limitations:
Localhost exceptions (Azure documentation).

Record the Client app Application (client) ID (for example, 4369008b-21fa-427c-abaa-


9b53bf58e538 ).

In Authentication > Platform configurations > Single-page application:

1. Confirm the redirect URI of https://localhost/authentication/login-callback is


present.
2. In the Implicit grant section, ensure that the checkboxes for Access tokens and ID
tokens aren't selected. Implicit grant isn't recommended for Blazor apps using
MSAL v2.0 or later. For more information, see Secure ASP.NET Core Blazor
WebAssembly.
3. The remaining defaults for the app are acceptable for this experience.
4. Select the Save button if you made changes.

In API permissions from the sidebar:

1. Select Add a permission followed by My APIs.


2. Select the Server API app from the Name column (for example, Blazor Server AAD
B2C).
3. Open the API list if it isn't already open.
4. Enable access to the API (for example, API.Access ) with the checkbox.
5. Select Add permissions.
6. Select the Grant admin consent for {TENANT NAME} button. Select Yes to
confirm.

) Important

If you don't have the authority to grant admin consent to the tenant in the last step
of API permissions configuration because consent to use the app is delegated to
users, then you must take the following additional steps:

The app must use a trusted publisher domain.


In the Server app's configuration in the Azure portal, select Expose an API.
Under Authorized client applications, select the button to Add a client
application. Add the Client app's Application (client) ID (for example,
4369008b-21fa-427c-abaa-9b53bf58e538 ).

Return to Azure AD B2C in the Azure portal. Select User flows and use the following
guidance: Create a sign-up and sign-in user flow. At a minimum, select Application
claims for the sign-up/sign-in user flow and then the Display Name user attribute
checkbox to populate the context.User.Identity?.Name / context.User.Identity.Name in
the LoginDisplay component ( Shared/LoginDisplay.razor ).

Record the sign-up and sign-in user flow name created for the app (for example,
B2C_1_signupsignin1 ).

Create the Blazor app


Replace the placeholders in the following command with the information recorded
earlier and execute the command in a command shell:

.NET CLI

dotnet new blazorwasm -au IndividualB2C --aad-b2c-instance "{AAD B2C


INSTANCE}" --api-client-id "{SERVER API APP CLIENT ID}" --app-id-uri "
{SERVER API APP ID URI GUID}" --client-id "{CLIENT APP CLIENT ID}" --
default-scope "{DEFAULT SCOPE}" --domain "{TENANT DOMAIN}" -ho -o {PROJECT
NAME} -ssp "{SIGN UP OR SIGN IN POLICY}"

2 Warning
Avoid using dashes ( - ) in the app name {PROJECT NAME} that break the formation
of the OIDC app identifier. Logic in the Blazor WebAssembly project template uses
the project name for an OIDC app identifier in the solution's configuration. Pascal
case ( BlazorSample ) or underscores ( Blazor_Sample ) are acceptable alternatives. For
more information, see Dashes in a hosted Blazor WebAssembly project name
break OIDC security (dotnet/aspnetcore #35337) .

Placeholder Azure portal name Example

{AAD B2C Instance https://contoso.b2clogin.com/ (includes the


INSTANCE} trailing slash)

{PROJECT — BlazorSample
NAME}

{CLIENT APP Application (client) ID for 4369008b-21fa-427c-abaa-9b53bf58e538


CLIENT ID} the Client app

{DEFAULT Scope name API.Access


SCOPE}

{SERVER API Application (client) ID for 41451fa7-82d9-4673-8fa5-69eff5a761fd


APP CLIENT ID} the Server app

{SERVER API Application ID URI GUID 41451fa7-82d9-4673-8fa5-69eff5a761fd (GUID ONLY,


APP ID URI by default matches the {SERVER API APP CLIENT
GUID} ID} )

{SIGN UP OR Sign-up/sign-in user B2C_1_signupsignin1


SIGN IN flow
POLICY}

{TENANT Primary/Publisher/Tenant contoso.onmicrosoft.com


DOMAIN} domain

The output location specified with the -o|--output option creates a project folder if it
doesn't exist and becomes part of the project's name. Avoid using dashes ( - ) in the
app name that break the formation of the OIDC app identifier (see the earlier
WARNING).

Run the app


Run the app from the Server project. When using Visual Studio, either:
Select the dropdown arrow next to the Run button. Open Configure Startup
Projects from the dropdown list. Select the Single startup project option. Confirm
or change the project for the startup project to the Server project.

Confirm that the Server project is highlighted in Solution Explorer before you
start the app with any of the following approaches:
Select the Run button.
Use Debug > Start Debugging from the menu.
Press F5 .

In a command shell, navigate to the Server project folder of the solution. Execute
the dotnet run command.

Custom policies
The Microsoft Authentication Library (Microsoft.Authentication.WebAssembly.Msal,
NuGet package ) doesn't support AAD B2C custom policies by default.

Configure User.Identity.Name
The guidance in this section covers optionally populating User.Identity.Name with the
value from the name claim.

By default, the Server app API populates User.Identity.Name with the value from the
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name claim type (for example,

2d64b3da-d9d5-42c6-9352-53d8df33d770@contoso.onmicrosoft.com ).

To configure the app to receive the value from the name claim type:

Add a namespace for Microsoft.AspNetCore.Authentication.JwtBearer to


Program.cs :

C#

using Microsoft.AspNetCore.Authentication.JwtBearer;

Configure the TokenValidationParameters.NameClaimType of the JwtBearerOptions


in Program.cs :

C#
builder.Services.Configure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.NameClaimType = "name";
});

Parts of the solution


This section describes the parts of a solution generated from the Blazor WebAssembly
project template and describes how the solution's Client and Server projects are
configured for reference. There's no specific guidance to follow in this section for a basic
working application if you created the app using the guidance in the Walkthrough
section. The guidance in this section is helpful for updating an app to authenticate and
authorize users. However, an alternative approach to updating an app is to create a new
app from the guidance in the Walkthrough section and moving the app's components,
classes, and resources to the new app.

appsettings.json configuration

This section pertains to the solution's Server app.

The appsettings.json file contains the options to configure the JWT bearer handler
used to validate access tokens:

JSON

{
"AzureAdB2C": {
"Instance": "https://{TENANT}.b2clogin.com/",
"ClientId": "{SERVER API APP CLIENT ID}",
"Domain": "{TENANT DOMAIN}",
"Scopes": "{DEFAULT SCOPE}",
"SignUpSignInPolicyId": "{SIGN UP OR SIGN IN POLICY}"
}
}

Example:

JSON

{
"AzureAdB2C": {
"Instance": "https://contoso.b2clogin.com/",
"ClientId": "41451fa7-82d9-4673-8fa5-69eff5a761fd",
"Domain": "contoso.onmicrosoft.com",
"Scopes": "API.Access",
"SignUpSignInPolicyId": "B2C_1_signupsignin1",
}
}

Authentication package
This section pertains to the solution's Server app.

The support for authenticating and authorizing calls to ASP.NET Core web APIs with the
Microsoft Identity Platform is provided by the Microsoft.Identity.Web package.

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 .

The Server app of a hosted Blazor solution created from the Blazor WebAssembly
template includes the Microsoft.Identity.Web.UI package by default. The package
adds UI for user authentication in web apps and isn't used by the Blazor framework. If
the Server app won't be used to authenticate users directly, it's safe to remove the
package reference from the Server app's project file.

Authentication service support


This section pertains to the solution's Server app.

The AddAuthentication method sets up authentication services within the app and
configures the JWT Bearer handler as the default authentication method. The
AddMicrosoftIdentityWebApi method configures services to protect the web API with
Microsoft Identity Platform v2.0. This method expects an AzureAdB2C section in the app's
configuration with the necessary settings to initialize authentication options.

C#

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAdB2C"));

7 Note
When a single authentication scheme is registered, the authentication scheme is
automatically used as the app's default scheme, and it isn't necessary to state the
scheme to AddAuthentication or via AuthenticationOptions. For more
information, see Overview of ASP.NET Core Authentication and the ASP.NET Core
announcement (aspnet/Announcements #490) .

UseAuthentication and UseAuthorization ensure that:

The app attempts to parse and validate tokens on incoming requests.


Any request attempting to access a protected resource without proper credentials
fails.

C#

app.UseAuthorization();

WeatherForecast controller
This section pertains to the solution's Server app.

The WeatherForecast controller ( Controllers/WeatherForecastController.cs ) exposes a


protected API with the [Authorize] attribute applied to the controller. It's important to
understand that:

The [Authorize] attribute in this API controller is the only thing that protects this
API from unauthorized access.
The [Authorize] attribute used in the Blazor WebAssembly app only serves as a hint
to the app that the user should be authorized for the app to work correctly.

C#

[Authorize]
[ApiController]
[Route("[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
...
}
}
wwwroot/appsettings.json configuration

This section pertains to the solution's Client app.

Configuration is supplied by the wwwroot/appsettings.json file:

JSON

{
"AzureAdB2C": {
"Authority": "{AAD B2C INSTANCE}{TENANT DOMAIN}/{SIGN UP OR SIGN IN
POLICY}",
"ClientId": "{CLIENT APP CLIENT ID}",
"ValidateAuthority": false
}
}

In the preceding configuration, the {AAD B2C INSTANCE} includes a trailing slash.

Example:

JSON

{
"AzureAdB2C": {
"Authority":
"https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_signupsignin1",
"ClientId": "4369008b-21fa-427c-abaa-9b53bf58e538",
"ValidateAuthority": false
}
}

Authentication package
This section pertains to the solution's Client app.

When an app is created to use an Individual B2C Account ( IndividualB2C ), the app
automatically receives a package reference for the Microsoft Authentication Library
(Microsoft.Authentication.WebAssembly.Msal ). The package provides a set of
primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the


Microsoft.Authentication.WebAssembly.Msal package to the app.

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 .

The Microsoft.Authentication.WebAssembly.Msal package transitively adds the


Microsoft.AspNetCore.Components.WebAssembly.Authentication package to the app.

Authentication service support


This section pertains to the solution's Client app.

Support for HttpClient instances is added that include access tokens when making
requests to the server project.

Program.cs :

C#

builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI", client =>


client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()


.CreateClient("{PROJECT NAME}.ServerAPI"));

The placeholder {PROJECT NAME} is the project name at solution creation. For example,
providing a project name of BlazorSample produces a named HttpClient of
BlazorSample.ServerAPI .

Support for authenticating users is registered in the service container with the
AddMsalAuthentication extension method provided by the
Microsoft.Authentication.WebAssembly.Msal package. This method sets up the
services required for the app to interact with the Identity Provider (IP).

Program.cs :

C#

builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C",
options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
The {SCOPE URI} is the default access token scope (for example,
https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access or
the custom URI that you configured in the Azure portal).

The AddMsalAuthentication method accepts a callback to configure the parameters


required to authenticate an app. The values required for configuring the app can be
obtained from the Azure Portal AAD configuration when you register the app.

Access token scopes


This section pertains to the solution's Client app.

The default access token scopes represent the list of access token scopes that are:

Included by default in the sign in request.


Used to provision an access token immediately after authentication.

All scopes must belong to the same app per Azure Active Directory rules. Additional
scopes can be added for additional API apps as needed:

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

Specify additional scopes with AdditionalScopesToConsent :

C#

options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");

Example default access token scope:

C#

options.ProviderOptions.DefaultAccessTokenScopes.Add(
"https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-
69eff5a761fd/API.Access");

For more information, see the following sections of the Additional scenarios article:
Request additional access tokens
Attach tokens to outgoing requests

Login mode
This section pertains to the solution's Client app.

The framework defaults to pop-up login mode and falls back to redirect login mode if a
pop-up can't be opened. Configure MSAL to use redirect login mode by setting the
LoginMode property of MsalProviderOptions to redirect :

C#

builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.LoginMode = "redirect";
});

The default setting is popup , and the string value isn't case-sensitive.

Imports file
This section pertains to the solution's Client app.

The Microsoft.AspNetCore.Components.Authorization namespace is made available


throughout the app via the _Imports.razor file:

razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index page
This section pertains to the solution's Client app.
The Index page ( wwwroot/index.html ) page includes a script that defines the
AuthenticationService in JavaScript. AuthenticationService handles the low-level
details of the OIDC protocol. The app internally calls methods defined in the script to
perform the authentication operations.

HTML

<script
src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationServic
e.js"></script>

App component
This section pertains to the solution's Client app.

The App component ( App.razor ) is similar to the App component found in Blazor Server
apps:

The CascadingAuthenticationState component manages exposing the


AuthenticationState to the rest of the app.
The AuthorizeRouteView component makes sure that the current user is
authorized to access a given page or otherwise renders the RedirectToLogin
component.
The RedirectToLogin component manages redirecting unauthorized users to the
login page.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
App component ( App.razor ) isn't shown in this section. To inspect the markup of the
component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the App component ( App.razor ) in the generated app.

Inspect the App component ( App.razor ) in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

RedirectToLogin component
This section pertains to the solution's Client app.

The RedirectToLogin component ( Shared/RedirectToLogin.razor ):

Manages redirecting unauthorized users to the login page.


The current URL that the user is attempting to access is maintained by so that they
can be returned to that page if authentication is successful using:
Navigation history state in ASP.NET Core 7.0 or later.
A query string in ASP.NET Core 6.0 or earlier.

Inspect the RedirectToLogin component in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

LoginDisplay component
This section pertains to the solution's Client app.

The LoginDisplay component ( Shared/LoginDisplay.razor ) is rendered in the


MainLayout component ( Shared/MainLayout.razor ) and manages the following

behaviors:

For authenticated users:


Displays the current user name.
Offers a link to the user profile page in ASP.NET Core Identity.
Offers a button to log out of the app.
For anonymous users:
Offers the option to register.
Offers the option to log in.
Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
LoginDisplay component isn't shown in this section. To inspect the markup of the
component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the LoginDisplay component in the generated app.

Inspect the LoginDisplay component in reference source . The templated


content for Hosted equal to true is used.

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

Authentication component
This section pertains to the solution's Client app.

The page produced by the Authentication component ( Pages/Authentication.razor )


defines the routes required for handling different authentication stages.

The RemoteAuthenticatorView component:

Is provided by the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package.
Manages performing the appropriate actions at each stage of authentication.

razor

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
[Parameter]
public string? Action { get; set; }
}
7 Note

Nullable reference types (NRTs) and .NET compiler null-state static analysis is
supported in ASP.NET Core 6.0 or later. Prior to the release of ASP.NET Core 6.0, the
string type appears without the null type designation ( ? ).

FetchData component
This section pertains to the solution's Client app.

The FetchData component shows how to:

Provision an access token.


Use the access token to call a protected resource API in the Server app.

The @attribute [Authorize] directive indicates to the Blazor WebAssembly authorization


system that the user must be authorized in order to visit this component. The presence
of the attribute in the Client app doesn't prevent the API on the server from being
called without proper credentials. The Server app also must use [Authorize] on the
appropriate endpoints to correctly protect them.

IAccessTokenProvider.RequestAccessToken takes care of requesting an access token that


can be added to the request to call the API. If the token is cached or the service is able
to provision a new access token without user interaction, the token request succeeds.
Otherwise, the token request fails with an AccessTokenNotAvailableException, which is
caught in a try-catch statement.

In order to obtain the actual token to include in the request, the app must check that
the request succeeded by calling tokenResult.TryGetToken(out var token).

If the request was successful, the token variable is populated with the access token. The
AccessToken.Value property of the token exposes the literal string to include in the
Authorization request header.

If the request failed because the token couldn't be provisioned without user interaction:

ASP.NET Core 7.0 or later: The app navigates to


AccessTokenResult.InteractiveRequestUrl using the given

AccessTokenResult.InteractionOptions to allow refreshing the access token.


ASP.NET Core 6.0 or earlier: The token result contains a redirect URL. Navigating to
this URL takes the user to the login page and back to the current page after a
successful authentication.
razor

@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
private WeatherForecast[] forecasts;

protected override async Task OnInitializedAsync()


{
try
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
("WeatherForecast");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}

Troubleshoot

Logging
This section applies to ASP.NET Core 7.0 or later.

To enable debug or trace logging for Blazor WebAssembly authentication, see ASP.NET
Core Blazor logging.

Common errors
Misconfiguration of the app or Identity Provider (IP)

The most common errors are caused by incorrect configuration. The following are
a few examples:
Depending on the requirements of the scenario, a missing or incorrect
Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI
prevents an app from authenticating clients.
An incorrect access token scope prevents clients from accessing server web API
endpoints.
Incorrect or missing server API permissions prevent clients from accessing
server web API endpoints.
Running the app at a different port than is configured in the Redirect URI of the
Identity Provider's app registration.

Configuration sections of this article's guidance show examples of the correct


configuration. Carefully check each section of the article looking for app and IP
misconfiguration.

If the configuration appears correct:

Analyze application logs.

Examine the network traffic between the client app and the IP or server app with
the browser's developer tools. Often, an exact error message or a message with
a clue to what's causing the problem is returned to the client by the IP or server
app after making a request. Developer tools guidance is found in the following
articles:
Google Chrome (Google documentation)
Microsoft Edge
Mozilla Firefox (Mozilla documentation)

Decode the contents of a JSON Web Token (JWT) used for authenticating a
client or accessing a server web API, depending on where the problem is
occurring. For more information, see Inspect the content of a JSON Web Token
(JWT).

The documentation team responds to document feedback and bugs in articles


(open an issue from the This page feedback section) but is unable to provide
product support. Several public support forums are available to assist with
troubleshooting an app. We recommend the following:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

The preceding forums are not owned or controlled by Microsoft.

For non-security, non-sensitive, and non-confidential reproducible framework bug


reports, open an issue with the ASP.NET Core product unit . Don't open an issue
with the product unit until you've thoroughly investigated the cause of a problem
and can't resolve it on your own and with the help of the community on a public
support forum. The product unit isn't able to troubleshoot individual apps that are
broken due to simple misconfiguration or use cases involving third-party services.
If a report is sensitive or confidential in nature or describes a potential security flaw
in the product that attackers may exploit, see Reporting security issues and bugs
(dotnet/aspnetcore GitHub repository) .

Unauthorized client for AAD

info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

Login callback error from AAD:


Error: unauthorized_client
Description: AADB2C90058: The provided application is not configured to
allow public clients.

To resolve the error:

1. In the Azure portal, access the app's manifest.


2. Set the allowPublicClient attribute to null or true .

Cookies and site data


Cookies and site data can persist across app updates and interfere with testing and
troubleshooting. Clear the following when making app code changes, user account
changes with the provider, or provider app configuration changes:

User sign-in cookies


App cookies
Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing
and troubleshooting is to:

Configure a browser
Use a browser for testing that you can configure to delete all cookie and site
data each time the browser is closed.
Make sure that the browser is closed manually or by the IDE for any change to
the app, test user, or provider configuration.
Use a custom command to open a browser in incognito or private mode in Visual
Studio:
Open Browse With dialog box from Visual Studio's Run button.
Select the Add button.
Provide the path to your browser in the Program field. The following executable
paths are typical installation locations for Windows 10. If your browser is
installed in a different location or you aren't using Windows 10, provide the
path to the browser's executable.
Microsoft Edge: C:\Program Files
(x86)\Microsoft\Edge\Application\msedge.exe

Google Chrome: C:\Program Files


(x86)\Google\Chrome\Application\chrome.exe

Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe


In the Arguments field, provide the command-line option that the browser uses
to open in incognito or private mode. Some browsers require the URL of the
app.
Microsoft Edge: Use -inprivate .
Google Chrome: Use --incognito --new-window {URL} , where the placeholder
{URL} is the URL to open (for example, https://localhost:5001 ).
Mozilla Firefox: Use -private -url {URL} , where the placeholder {URL} is the
URL to open (for example, https://localhost:5001 ).
Provide a name in the Friendly name field. For example, Firefox Auth Testing .
Select the OK button.
To avoid having to select the browser profile for each iteration of testing with an
app, set the profile as the default with the Set as Default button.
Make sure that the browser is closed by the IDE for any change to the app, test
user, or provider configuration.

App upgrades
A functioning app may fail immediately after upgrading either the .NET Core SDK on the
development machine or changing package versions within the app. In some cases,
incoherent packages may break an app when performing major upgrades. Most of these
issues can be fixed by following these instructions:

1. Clear the local system's NuGet package caches by executing dotnet nuget locals all
--clear from a command shell.
2. Delete the project's bin and obj folders.
3. Restore and rebuild the project.
4. Delete all of the files in the deployment folder on the server prior to redeploying
the app.
7 Note

Use of package versions incompatible with the app's target framework isn't
supported. For information on a package, use the NuGet Gallery or FuGet
Package Explorer .

Run the Server app


When testing and troubleshooting a hosted Blazor WebAssembly solution, make sure
that you're running the app from the Server project.

Inspect the user


The ASP.NET Core framework's test assets include a Blazor WebAssembly client app
with a User component that can be useful in troubleshooting. The User component can
be used directly in apps or serve as the basis for further customization:

User test component in the dotnet/aspnetcore GitHub repository

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Inspect the content of a JSON Web Token (JWT)


To decode a JSON Web Token (JWT), use Microsoft's jwt.ms tool. Values in the UI
never leave your browser.

Example encoded JWT (shortened for display):

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0
djhkbE5QNC1j ...
bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzp
D-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-
yBMKV2_nXA25Q
Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

JSON

{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1610059429,
"nbf": 1610055829,
"ver": "1.0",
"iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-
226dcc9ad298/v2.0/",
"sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
"aud": "70bde375-fce3-4b82-984a-b247d823a03f",
"nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
"iat": 1610055829,
"auth_time": 1610055822,
"idp": "idp.com",
"tfp": "B2C_1_signupsignin"
}.[Signature]

Additional resources
Configure an app's publisher domain
Azure Active Directory app manifest: identifierUris attribute
ASP.NET Core Blazor WebAssembly additional security scenarios
Build a custom version of the Authentication.MSAL JavaScript library
Unauthenticated or unauthorized web API requests in an app with a secure default
client
Cloud authentication with Azure Active Directory B2C in ASP.NET Core
Tutorial: Create an Azure Active Directory B2C tenant
Tutorial: Register an application in Azure Active Directory B2C
Microsoft identity platform documentation
Secure a hosted ASP.NET Core Blazor
WebAssembly app with Identity Server
Article • 03/24/2023

This article explains how to create a hosted Blazor WebAssembly solution that uses
Duende Identity Server to authenticate users and API calls.

) 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.

7 Note

To configure a standalone or hosted Blazor WebAssembly app to use an existing,


external Identity Server instance, follow the guidance in Secure an ASP.NET Core
Blazor WebAssembly standalone app with the Authentication library.

For additional security scenario coverage after reading this article, see ASP.NET Core
Blazor WebAssembly additional security scenarios.

Walkthrough
The subsections of the walkthrough explain how to:

Create the Blazor app


Run the app

Create a Blazor app

Visual Studio

To create a new Blazor WebAssembly project with an authentication mechanism:

1. Create a new project.

2. Choose the Blazor WebAssembly App template. Select Next.


3. Provide a Project name without using dashes. Confirm that the Location is
correct. Select Next.

Avoid using dashes ( - ) in the project name that break the formation of the
OIDC app identifier. Logic in the Blazor WebAssembly project template uses
the project name for an OIDC app identifier in the solution's configuration,
and dashes aren't permitted in an OIDC app identifier. Pascal case
( BlazorSample ) or underscores ( Blazor_Sample ) are acceptable alternatives.

4. In the Additional information dialog, select Individual Accounts as the


Authentication type to store users within the app using ASP.NET Core's
Identity system.

5. Select the ASP.NET Core Hosted checkbox.

6. Select the Create button to create the app.

Run the app


Run the app from the Server project. When using Visual Studio, either:

Select the dropdown arrow next to the Run button. Open Configure Startup
Projects from the dropdown list. Select the Single startup project option. Confirm
or change the project for the startup project to the Server project.

Confirm that the Server project is highlighted in Solution Explorer before you
start the app with any of the following approaches:
Select the Run button.
Use Debug > Start Debugging from the menu.
Press F5 .

In a command shell, navigate to the Server project folder of the solution. Execute
the dotnet run command.

Parts of the solution


This section describes the parts of a solution generated from the Blazor WebAssembly
project template and describes how the solution's Client and Server projects are
configured for reference. There's no specific guidance to follow in this section for a basic
working application if you created the app using the guidance in the Walkthrough
section. The guidance in this section is helpful for updating an app to authenticate and
authorize users. However, an alternative approach to updating an app is to create a new
app from the guidance in the Walkthrough section and moving the app's components,
classes, and resources to the new app.

Server app services


This section pertains to the solution's Server app.

The following services are registered.

In Program.cs :

Entity Framework Core and ASP.NET Core Identity:

C#

builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite( ... ));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();

Identity Server with an additional AddApiAuthorization helper method that sets


up default ASP.NET Core conventions on top of Identity Server:

C#

builder.Services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

Authentication with an additional AddIdentityServerJwt helper method that


configures the app to validate JWT tokens produced by Identity Server:

C#

builder.Services.AddAuthentication()
.AddIdentityServerJwt();

7 Note

When a single authentication scheme is registered, the authentication scheme is


automatically used as the app's default scheme, and it isn't necessary to state the
scheme to AddAuthentication or via AuthenticationOptions. For more
information, see Overview of ASP.NET Core Authentication and the ASP.NET Core
announcement (aspnet/Announcements #490) .

In Program.cs :

The Identity Server Middleware exposes the OpenID Connect (OIDC) endpoints:

C#

app.UseIdentityServer();

Authorization Middleware enables authorization capabilities:

C#

app.UseAuthorization();

API authorization
This section pertains to the solution's Server app.

The AddApiAuthorization helper method configures Identity Server for ASP.NET Core
scenarios. Identity Server is a powerful and extensible framework for handling app
security concerns. Identity Server exposes unnecessary complexity for the most common
scenarios. Consequently, a set of conventions and configuration options is provided that
we consider a good starting point. Once your authentication needs change, the full
power of Identity Server is available to customize authentication to suit an app's
requirements.

Add an authentication handler for an API that coexists


with Identity Server
This section pertains to the solution's Server app.

The AddIdentityServerJwt helper method configures a policy scheme for the app as the
default authentication handler. The policy is configured to allow Identity to handle all
requests routed to any subpath in the Identity URL space under /Identity . The
JwtBearerHandler handles all other requests. Additionally, this method:
Registers an API resource with Identity Server with a default scope of {PROJECT
NAME}API , where the {PROJECT NAME} placeholder is the project's name at app
creation.
Configures the JWT Bearer Token Middleware to validate tokens issued by Identity
Server for the app.

Weather forecast controller


This section pertains to the solution's Server app.

In the WeatherForecastController ( Controllers/WeatherForecastController.cs ), the


[Authorize] attribute is applied to the class. The attribute indicates that the user must be
authorized based on the default policy to access the resource. The default authorization
policy is configured to use the default authentication scheme, which is set up by
AddIdentityServerJwt. The helper method configures JwtBearerHandler as the default
handler for requests to the app.

Application database context


This section pertains to the solution's Server app.

In the ApplicationDbContext ( Data/ApplicationDbContext.cs ), DbContext extends


ApiAuthorizationDbContext<TUser> to include the schema for Identity Server.
ApiAuthorizationDbContext<TUser> is derived from IdentityDbContext.

To gain full control of the database schema, inherit from one of the available Identity
DbContext classes and configure the context to include the Identity schema by calling
builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value) in the

OnModelCreating method.

OIDC configuration controller


This section pertains to the solution's Server app.

In the OidcConfigurationController ( Controllers/OidcConfigurationController.cs ), the


client endpoint is provisioned to serve OIDC parameters.

App settings
This section pertains to the solution's Server app.
In the app settings file ( appsettings.json ) at the project root, the IdentityServer
section describes the list of configured clients. In the following example, there's a single
client. The client name corresponds to the Client app's assembly name and is mapped
by convention to the OAuth ClientId parameter. The profile indicates the app type
being configured. The profile is used internally to drive conventions that simplify the
configuration process for the server.

JSON

"IdentityServer": {
"Clients": {
"{ASSEMBLY NAME}": {
"Profile": "IdentityServerSPA"
}
}
}

The placeholder {ASSEMBLY NAME} is the Client app's assembly name (for example,
BlazorSample.Client ).

Authentication package
This section pertains to the solution's Client app.

When an app is created to use Individual User Accounts ( Individual ), the app
automatically receives a package reference for the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package. The
package provides a set of primitives that help the app authenticate users and obtain
tokens to call protected APIs.

If adding authentication to an app, manually add the


Microsoft.AspNetCore.Components.WebAssembly.Authentication package to the app.

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 .

HttpClient configuration

This section pertains to the solution's Client app.


In Program.cs , a named HttpClient is configured to supply HttpClient instances that
include access tokens when making requests to the server API. By default at solution
creation, the named HttpClient is {PROJECT NAME}.ServerAPI , where the {PROJECT NAME}
placeholder is the project's name.

C#

builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI",
client => client.BaseAddress = new
Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()


.CreateClient("{PROJECT NAME}.ServerAPI"));

The placeholder {PROJECT NAME} is the project name at solution creation. For example,
providing a project name of BlazorSample produces a named HttpClient of
BlazorSample.ServerAPI .

7 Note

If you're configuring a Blazor WebAssembly app to use an existing Identity Server


instance that isn't part of a hosted Blazor solution, change the HttpClient base
address registration from IWebAssemblyHostEnvironment.BaseAddress
( builder.HostEnvironment.BaseAddress ) to the server app's API authorization
endpoint URL.

API authorization support


This section pertains to the solution's Client app.

The support for authenticating users is plugged into the service container by the
extension method provided inside the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package. This
method sets up the services required by the app to interact with the existing
authorization system.

C#

builder.Services.AddApiAuthorization();
By default, configuration for the app is loaded by convention from
_configuration/{client-id} . By convention, the client ID is set to the app's assembly
name. This URL can be changed to point to a separate endpoint by calling the overload
with options.

Imports file

This section pertains to the solution's Client app.

The Microsoft.AspNetCore.Components.Authorization namespace is made available


throughout the app via the _Imports.razor file:

razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index page

This section pertains to the solution's Client app.

The Index page ( wwwroot/index.html ) page includes a script that defines the
AuthenticationService in JavaScript. AuthenticationService handles the low-level
details of the OIDC protocol. The app internally calls methods defined in the script to
perform the authentication operations.

HTML

<script
src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/Aut
henticationService.js"></script>

App component

This section pertains to the solution's Client app.


The App component ( App.razor ) is similar to the App component found in Blazor Server
apps:

The CascadingAuthenticationState component manages exposing the


AuthenticationState to the rest of the app.
The AuthorizeRouteView component makes sure that the current user is
authorized to access a given page or otherwise renders the RedirectToLogin
component.
The RedirectToLogin component manages redirecting unauthorized users to the
login page.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
App component ( App.razor ) isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the App component ( App.razor ) in the generated app.

Inspect the App component ( App.razor ) in reference source .

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

RedirectToLogin component

This section pertains to the solution's Client app.

The RedirectToLogin component ( Shared/RedirectToLogin.razor ):

Manages redirecting unauthorized users to the login page.


The current URL that the user is attempting to access is maintained by so that they
can be returned to that page if authentication is successful using:
Navigation history state in ASP.NET Core 7.0 or later.
A query string in ASP.NET Core 6.0 or earlier.

Inspect the RedirectToLogin component in reference source .


7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

LoginDisplay component

This section pertains to the solution's Client app.

The LoginDisplay component ( Shared/LoginDisplay.razor ) is rendered in the


MainLayout component ( Shared/MainLayout.razor ) and manages the following

behaviors:

For authenticated users:


Displays the current user name.
Offers a link to the user profile page in ASP.NET Core Identity.
Offers a button to log out of the app.
For anonymous users:
Offers the option to register.
Offers the option to log in.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the
LoginDisplay component isn't shown in this section. To inspect the markup of the

component for a given release, use either of the following approaches:

Create an app provisioned for authentication from the default Blazor


WebAssembly project template for the version of ASP.NET Core that you intend to
use. Inspect the LoginDisplay component in the generated app.

Inspect the LoginDisplay component in reference source . The templated


content for Hosted equal to true is used.

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

Authentication component

This section pertains to the solution's Client app.

The page produced by the Authentication component ( Pages/Authentication.razor )


defines the routes required for handling different authentication stages.

The RemoteAuthenticatorView component:

Is provided by the
Microsoft.AspNetCore.Components.WebAssembly.Authentication package.
Manages performing the appropriate actions at each stage of authentication.

razor

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
[Parameter]
public string? Action { get; set; }
}

7 Note

Nullable reference types (NRTs) and .NET compiler null-state static analysis is
supported in ASP.NET Core 6.0 or later. Prior to the release of ASP.NET Core 6.0, the
string type appears without the null type designation ( ? ).

FetchData component

This section pertains to the solution's Client app.

The FetchData component shows how to:

Provision an access token.


Use the access token to call a protected resource API in the Server app.
The @attribute [Authorize] directive indicates to the Blazor WebAssembly authorization
system that the user must be authorized in order to visit this component. The presence
of the attribute in the Client app doesn't prevent the API on the server from being
called without proper credentials. The Server app also must use [Authorize] on the
appropriate endpoints to correctly protect them.

IAccessTokenProvider.RequestAccessToken takes care of requesting an access token that


can be added to the request to call the API. If the token is cached or the service is able
to provision a new access token without user interaction, the token request succeeds.
Otherwise, the token request fails with an AccessTokenNotAvailableException, which is
caught in a try-catch statement.

In order to obtain the actual token to include in the request, the app must check that
the request succeeded by calling tokenResult.TryGetToken(out var token).

If the request was successful, the token variable is populated with the access token. The
AccessToken.Value property of the token exposes the literal string to include in the
Authorization request header.

If the request failed because the token couldn't be provisioned without user interaction:

ASP.NET Core 7.0 or later: The app navigates to


AccessTokenResult.InteractiveRequestUrl using the given

AccessTokenResult.InteractionOptions to allow refreshing the access token.


ASP.NET Core 6.0 or earlier: The token result contains a redirect URL. Navigating to
this URL takes the user to the login page and back to the current page after a
successful authentication.

razor

@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
private WeatherForecast[] forecasts;

protected override async Task OnInitializedAsync()


{
try
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
("WeatherForecast");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}

Azure App Service on Linux


Specify the issuer explicitly when deploying to Azure App Service on Linux. For more
information, see Introduction to authentication for Single Page Apps on ASP.NET Core.

Name and role claim with API authorization

Custom user factory


In the Client app, create a custom user factory. Identity Server sends multiple roles as a
JSON array in a single role claim. A single role is sent as a string value in the claim. The
factory creates an individual role claim for each of the user's roles.

CustomUserFactory.cs :

C#

using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomUserFactory


: AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public CustomUserFactory(IAccessTokenProviderAccessor accessor)
: base(accessor)
{
}

public override async ValueTask<ClaimsPrincipal> CreateUserAsync(


RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);

if (user.Identity is not null && user.Identity.IsAuthenticated)


{
var identity = (ClaimsIdentity)user.Identity;
var roleClaims =
identity.FindAll(identity.RoleClaimType).ToArray();

if (roleClaims.Any())
{
foreach (var existingClaim in roleClaims)
{
identity.RemoveClaim(existingClaim);
}

var rolesElem =
account.AdditionalProperties[identity.RoleClaimType];

if (options.RoleClaim is not null && rolesElem is


JsonElement roles)
{
if (roles.ValueKind == JsonValueKind.Array)
{
foreach (var role in roles.EnumerateArray())
{
var roleValue = role.GetString();

if (!string.IsNullOrEmpty(roleValue))
{
identity.AddClaim(
new Claim(options.RoleClaim, roleValue));
}

}
}
else
{
var roleValue = roles.GetString();

if (!string.IsNullOrEmpty(roleValue))
{
identity.AddClaim(
new Claim(options.RoleClaim, roleValue));
}
}
}
}
}

return user;
}
}

In the Client app, register the factory in Program.cs :

C#
builder.Services.AddApiAuthorization()
.AddAccountClaimsPrincipalFactory<CustomUserFactory>();

In the Server app, call AddRoles on the Identity builder, which adds role-related services.

In Program.cs :

C#

using Microsoft.AspNetCore.Identity;

...

builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

Configure Identity Server


Use one of the following approaches:

API authorization options


Profile Service

API authorization options


In the Server app:

Configure Identity Server to put the name and role claims into the ID token and
access token.
Prevent the default mapping for roles in the JWT token handler.

In Program.cs :

C#

using System.IdentityModel.Tokens.Jwt;

...

builder.Services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.IdentityResources["openid"].UserClaims.Add("name");
options.ApiResources.Single().UserClaims.Add("name");
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

Profile Service
In the Server app, create a ProfileService implementation.

ProfileService.cs :

C#

using IdentityModel;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;

public class ProfileService : IProfileService


{
public ProfileService()
{
}

public async Task GetProfileDataAsync(ProfileDataRequestContext context)


{
var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
context.IssuedClaims.AddRange(nameClaim);

var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);


context.IssuedClaims.AddRange(roleClaims);

await Task.CompletedTask;
}

public async Task IsActiveAsync(IsActiveContext context)


{
await Task.CompletedTask;
}
}

In the Server app, register the Profile Service in Program.cs :

C#

using Duende.IdentityServer.Services;

...

builder.Services.AddTransient<IProfileService, ProfileService>();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

Use authorization mechanisms


In the Client app, component authorization approaches are functional at this point. Any
of the authorization mechanisms in components can use a role to authorize the user:

AuthorizeView component (Example: <AuthorizeView Roles="Admin"> )

[Authorize] attribute directive (AuthorizeAttribute) (Example: @attribute


[Authorize(Roles = "Admin")] )

Procedural logic (Example: if (user.IsInRole("Admin")) { ... } )

Multiple role tests are supported:

C#

if (user.IsInRole("Admin") && user.IsInRole("Developer"))


{
...
}

User.Identity.Name is populated in the Client app with the user's user name, which is

usually their sign-in email address.

UserManager and SignInManager


Set the user identifier claim type when a Server app requires:

UserManager<TUser> or SignInManager<TUser> in an API endpoint.


IdentityUser details, such as the user's name, email address, or lockout end time.

In Program.cs for ASP.NET Core 6.0 or later:

C#

using System.Security.Claims;

...

builder.Services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
In Startup.ConfigureServices for versions of ASP.NET Core earlier than 6.0:

C#

using System.Security.Claims;

...

services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

The following WeatherForecastController logs the UserName when the Get method is
called.

7 Note

The following example uses a file-scoped namespace, which is a C# 10 or later


(.NET 6 or later) feature.

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using BlazorSample.Server.Models;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly UserManager<ApplicationUser> userManager;

private static readonly string[] Summaries = new[]


{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
"Balmy", "Hot", "Sweltering", "Scorching"
};

private readonly ILogger<WeatherForecastController> logger;


public WeatherForecastController(ILogger<WeatherForecastController>
logger,
UserManager<ApplicationUser> userManager)
{
this.logger = logger;
this.userManager = userManager;
}

[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
var rng = new Random();

var user = await userManager.GetUserAsync(User);

if (user != null)
{
logger.LogInformation($"User.Identity.Name: {user.UserName}");
}

return Enumerable.Range(1, 5).Select(index => new WeatherForecast


{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}

In the preceding example:

The Server project's namespace is BlazorSample.Server .


The Shared project's namespace is BlazorSample.Shared .

Host in Azure App Service with a custom


domain and certificate
The following guidance explains:

How to deploy a hosted Blazor WebAssembly app with Identity Server to Azure
App Service with a custom domain.
How to create and use a TLS certificate for HTTPS protocol communication with
browsers. Although the guidance focuses on using the certificate with a custom
domain, the guidance is equally applicable to using a default Azure Apps domain,
for example contoso.azurewebsites.net .
For this hosting scenario, do not use the same certificate for Identity Server's token
signing key and the site's HTTPS secure communication with browsers:

Using different certificates for these two requirements is a good security practice
because it isolates private keys for each purpose.
TLS certificates for communication with browsers is managed independently
without affecting Identity Server's token signing.
When Azure Key Vault supplies a certificate to an App Service app for custom
domain binding, Identity Server can't obtain the same certificate from Azure Key
Vault for token signing. Although configuring Identity Server to use the same TLS
certificate from a physical path is possible, placing security certificates into source
control is a poor practice and should be avoided in most scenarios.

In the following guidance, a self-signed certificate is created in Azure Key Vault solely for
Identity Server token signing. The Identity Server configuration uses the key vault
certificate via the app's CurrentUser > My certificate store. Other certificates used for
HTTPS traffic with custom domains are created and configured separately from the
Identity Server signing certificate.

To configure an app, Azure App Service, and Azure Key Vault to host with a custom
domain and HTTPS:

1. Create an App Service plan with an plan level of Basic B1 or higher. App Service
requires a Basic B1 or higher service tier to use custom domains.

2. Create a PFX certificate for the site's secure browser communication (HTTPS
protocol) with a common name of the site's fully qualified domain name (FQDN)
that your organization controls (for example, www.contoso.com ). Create the
certificate with:

Key uses
Digital signature validation ( digitalSignature )
Key encipherment ( keyEncipherment )
Enhanced/extended key uses
Client Authentication (1.3.6.1.5.5.7.3.2)
Server Authentication (1.3.6.1.5.5.7.3.1)

To create the certificate, use one of the following approaches or any other suitable
tool or online service:

Azure Key Vault


MakeCert on Windows
OpenSSL
Make note of the password, which is used later to import the certificate into Azure
Key Vault.

For more information on Azure Key Vault certificates, see Azure Key Vault:
Certificates.

3. Create a new Azure Key Vault or use an existing key vault in your Azure
subscription.

4. In the key vault's Certificates area, import the PFX site certificate. Record the
certificate's thumbprint, which is used in the app's configuration later.

5. In Azure Key Vault, generate a new self-signed certificate for Identity Server token
signing. Give the certificate a Certificate Name and Subject. The Subject is
specified as CN={COMMON NAME} , where the {COMMON NAME} placeholder is the
certificate's common name. The common name can be any alphanumeric string.
For example, CN=IdentityServerSigning is a valid certificate Subject. In Issuance
Policy > Advanced Policy Configuration, use the default settings. Record the
certificate's thumbprint, which is used in the app's configuration later.

6. Navigate to Azure App Service in the Azure portal and create a new App Service
with the following configuration:

Publish set to Code .


Runtime stack set to the app's runtime.
For Sku and size, confirm that the App Service tier is Basic B1 or higher. App
Service requires a Basic B1 or higher service tier to use custom domains.

7. After Azure creates the App Service, open the app's Configuration and add a new
application setting specifying the certificate thumbprints recorded earlier. The app
setting key is WEBSITE_LOAD_CERTIFICATES . Separate the certificate thumbprints in
the app setting value with a comma, as the following example shows:

Key: WEBSITE_LOAD_CERTIFICATES
Value:
57443A552A46DB...D55E28D412B943565,29F43A772CB6AF...1D04F0C67F85FB0B1

In the Azure portal, saving app settings is a two-step process: Save the
WEBSITE_LOAD_CERTIFICATES key-value setting, then select the Save button at the

top of the blade.

8. Select the app's TLS/SSL settings. Select Private Key Certificates (.pfx). Use the
Import Key Vault Certificate process. Use the process twice to import both the
site's certificate for HTTPS communication and the site's self-signed Identity
Server token signing certificate.

9. Navigate to the Custom domains blade. At your domain registrar's website, use
the IP address and Custom Domain Verification ID to configure the domain. A
typical domain configuration includes:

An A Record with a Host of @ and a value of the IP address from the Azure
portal.
A TXT Record with a Host of asuid and the value of the verification ID
generated by Azure and provided by the Azure portal.

Make sure that you save the changes at your domain registrar's website correctly.
Some registrar websites require a two-step process to save domain records: One
or more records are saved individually followed by updating the domain's
registration with a separate button.

10. Return to the Custom domains blade in the Azure portal. Select Add custom
domain. Select the A Record option. Provide the domain and select Validate. If the
domain records are correct and propagated across the Internet, the portal allows
you to select the Add custom domain button.

It can take a few days for domain registration changes to propagate across
Internet domain name servers (DNS) after they're processed by your domain
registrar. If domain records aren't updated within three business days, confirm the
records are correctly set with the domain registrar and contact their customer
support.

11. In the Custom domains blade, the SSL STATE for the domain is marked Not
Secure . Select the Add binding link. Select the site HTTPS certificate from the key
vault for the custom domain binding.

12. In Visual Studio, open the Server project's app settings file ( appsettings.json or
appsettings.Production.json ). In the Identity Server configuration, add the

following Key section. Specify the self-signed certificate Subject for the Name key.
In the following example, the certificate's common name assigned in the key vault
is IdentityServerSigning , which yields a Subject of CN=IdentityServerSigning :

JSON

"IdentityServer": {

...
"Key": {
"Type": "Store",
"StoreName": "My",
"StoreLocation": "CurrentUser",
"Name": "CN=IdentityServerSigning"
}
},

13. In Visual Studio, create an Azure App Service publish profile for the Server project.
From the menu bar, select: Build > Publish > New > Azure > Azure App Service
(Windows or Linux). When Visual Studio is connected to an Azure subscription, you
can set the View of Azure resources by Resource type. Navigate within the Web
App list to find the App Service for the app and select it. Select Finish.

14. When Visual Studio returns to the Publish window, the key vault and SQL Server
database service dependencies are automatically detected.

No configuration changes to the default settings are required for the key vault
service.

For testing purposes, an app's local SQLite database, which is configured by


default by the Blazor template, can be deployed with the app without additional
configuration. Configuring a different database for Identity Server in production is
beyond the scope of this article. For more information, see the database resources
in the following documentation sets:

App Service
Duende Identity Server

15. Select the Edit link under the deployment profile name at the top of the window.
Change the destination URL to the site's custom domain URL (for example,
https://www.contoso.com ). Save the settings.

16. Publish the app. Visual Studio opens a browser window and requests the site at its
custom domain.

The Azure documentation contains additional detail on using Azure services and custom
domains with TLS binding in App Service, including information on using CNAME
records instead of A records. For more information, see the following resources:

App Service documentation


Tutorial: Map an existing custom DNS name to Azure App Service
Secure a custom DNS name with a TLS/SSL binding in Azure App Service
Azure Key Vault
We recommend using a new in-private or incognito browser window for each app test
run after a change to the app, app configuration, or Azure services in the Azure portal.
Lingering cookies from a previous test run can result in failed authentication or
authorization when testing the site even when the site's configuration is correct. For
more information on how to configure Visual Studio to open a new in-private or
incognito browser window for each test run, see the Cookies and site data section.

When App Service configuration is changed in the Azure portal, the updates generally
take effect quickly but aren't instant. Sometimes, you must wait a short period for an
App Service to restart in order for a configuration change to take effect.

If troubleshooting an Identity Server key-signing certificate loading problem, execute


the following command in an Azure portal Kudu PowerShell command shell. The
command provides a list of certificates that the app can access from the CurrentUser >
My certificate store. The output includes certificate subjects and thumbprints useful

when debugging an app:

PowerShell

Get-ChildItem -path Cert:\CurrentUser\My -Recurse | Format-List DnsNameList,


Subject, Thumbprint, EnhancedKeyUsageList

Troubleshoot

Logging
This section applies to ASP.NET Core 7.0 or later.

To enable debug or trace logging for Blazor WebAssembly authentication, see ASP.NET
Core Blazor logging.

Common errors
Misconfiguration of the app or Identity Provider (IP)

The most common errors are caused by incorrect configuration. The following are
a few examples:
Depending on the requirements of the scenario, a missing or incorrect
Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI
prevents an app from authenticating clients.
An incorrect access token scope prevents clients from accessing server web API
endpoints.
Incorrect or missing server API permissions prevent clients from accessing
server web API endpoints.
Running the app at a different port than is configured in the Redirect URI of the
Identity Provider's app registration.

Configuration sections of this article's guidance show examples of the correct


configuration. Carefully check each section of the article looking for app and IP
misconfiguration.

If the configuration appears correct:

Analyze application logs.

Examine the network traffic between the client app and the IP or server app with
the browser's developer tools. Often, an exact error message or a message with
a clue to what's causing the problem is returned to the client by the IP or server
app after making a request. Developer tools guidance is found in the following
articles:
Google Chrome (Google documentation)
Microsoft Edge
Mozilla Firefox (Mozilla documentation)

Decode the contents of a JSON Web Token (JWT) used for authenticating a
client or accessing a server web API, depending on where the problem is
occurring. For more information, see Inspect the content of a JSON Web Token
(JWT).

The documentation team responds to document feedback and bugs in articles


(open an issue from the This page feedback section) but is unable to provide
product support. Several public support forums are available to assist with
troubleshooting an app. We recommend the following:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

The preceding forums are not owned or controlled by Microsoft.

For non-security, non-sensitive, and non-confidential reproducible framework bug


reports, open an issue with the ASP.NET Core product unit . Don't open an issue
with the product unit until you've thoroughly investigated the cause of a problem
and can't resolve it on your own and with the help of the community on a public
support forum. The product unit isn't able to troubleshoot individual apps that are
broken due to simple misconfiguration or use cases involving third-party services.
If a report is sensitive or confidential in nature or describes a potential security flaw
in the product that attackers may exploit, see Reporting security issues and bugs
(dotnet/aspnetcore GitHub repository) .

Unauthorized client for AAD

info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

Login callback error from AAD:


Error: unauthorized_client
Description: AADB2C90058: The provided application is not configured to
allow public clients.

To resolve the error:

1. In the Azure portal, access the app's manifest.


2. Set the allowPublicClient attribute to null or true .

Cookies and site data


Cookies and site data can persist across app updates and interfere with testing and
troubleshooting. Clear the following when making app code changes, user account
changes with the provider, or provider app configuration changes:

User sign-in cookies


App cookies
Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing
and troubleshooting is to:

Configure a browser
Use a browser for testing that you can configure to delete all cookie and site
data each time the browser is closed.
Make sure that the browser is closed manually or by the IDE for any change to
the app, test user, or provider configuration.
Use a custom command to open a browser in incognito or private mode in Visual
Studio:
Open Browse With dialog box from Visual Studio's Run button.
Select the Add button.
Provide the path to your browser in the Program field. The following executable
paths are typical installation locations for Windows 10. If your browser is
installed in a different location or you aren't using Windows 10, provide the
path to the browser's executable.
Microsoft Edge: C:\Program Files
(x86)\Microsoft\Edge\Application\msedge.exe

Google Chrome: C:\Program Files


(x86)\Google\Chrome\Application\chrome.exe

Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe


In the Arguments field, provide the command-line option that the browser uses
to open in incognito or private mode. Some browsers require the URL of the
app.
Microsoft Edge: Use -inprivate .
Google Chrome: Use --incognito --new-window {URL} , where the placeholder
{URL} is the URL to open (for example, https://localhost:5001 ).
Mozilla Firefox: Use -private -url {URL} , where the placeholder {URL} is the
URL to open (for example, https://localhost:5001 ).
Provide a name in the Friendly name field. For example, Firefox Auth Testing .
Select the OK button.
To avoid having to select the browser profile for each iteration of testing with an
app, set the profile as the default with the Set as Default button.
Make sure that the browser is closed by the IDE for any change to the app, test
user, or provider configuration.

App upgrades
A functioning app may fail immediately after upgrading either the .NET Core SDK on the
development machine or changing package versions within the app. In some cases,
incoherent packages may break an app when performing major upgrades. Most of these
issues can be fixed by following these instructions:

1. Clear the local system's NuGet package caches by executing dotnet nuget locals all
--clear from a command shell.
2. Delete the project's bin and obj folders.
3. Restore and rebuild the project.
4. Delete all of the files in the deployment folder on the server prior to redeploying
the app.
7 Note

Use of package versions incompatible with the app's target framework isn't
supported. For information on a package, use the NuGet Gallery or FuGet
Package Explorer .

Run the Server app


When testing and troubleshooting a hosted Blazor WebAssembly solution, make sure
that you're running the app from the Server project.

Inspect the user


The ASP.NET Core framework's test assets include a Blazor WebAssembly client app
with a User component that can be useful in troubleshooting. The User component can
be used directly in apps or serve as the basis for further customization:

User test component in the dotnet/aspnetcore GitHub repository

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Inspect the content of a JSON Web Token (JWT)


To decode a JSON Web Token (JWT), use Microsoft's jwt.ms tool. Values in the UI
never leave your browser.

Example encoded JWT (shortened for display):

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0
djhkbE5QNC1j ...
bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzp
D-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-
yBMKV2_nXA25Q
Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

JSON

{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1610059429,
"nbf": 1610055829,
"ver": "1.0",
"iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-
226dcc9ad298/v2.0/",
"sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
"aud": "70bde375-fce3-4b82-984a-b247d823a03f",
"nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
"iat": 1610055829,
"auth_time": 1610055822,
"idp": "idp.com",
"tfp": "B2C_1_signupsignin"
}.[Signature]

Additional resources
Deployment to Azure App Service
Import a certificate from Key Vault (Azure documentation)
ASP.NET Core Blazor WebAssembly additional security scenarios
Unauthenticated or unauthorized web API requests in an app with a secure default
client
Configure ASP.NET Core to work with proxy servers and load balancers: Includes
guidance on:
Using Forwarded Headers Middleware to preserve HTTPS scheme information
across proxy servers and internal networks.
Additional scenarios and use cases, including manual scheme configuration,
request path changes for correct request routing, and forwarding the request
scheme for Linux and non-IIS reverse proxies.
Duende Identity Server
ASP.NET Core Blazor WebAssembly
additional security scenarios
Article • 06/22/2023

This article describes additional security scenarios for Blazor WebAssembly apps.

Attach tokens to outgoing requests


AuthorizationMessageHandler is a DelegatingHandler used to process access tokens.
Tokens are acquired using the IAccessTokenProvider service, which is registered by the
framework. If a token can't be acquired, an AccessTokenNotAvailableException is
thrown. AccessTokenNotAvailableException has a Redirect method that navigates to
AccessTokenResult.InteractiveRequestUrl using the given

AccessTokenResult.InteractionOptions to allow refreshing the access token.

For convenience, the framework provides the


BaseAddressAuthorizationMessageHandler preconfigured with the app's base address
as an authorized URL. Access tokens are only added when the request URI is within the
app's base URI. When outgoing request URIs aren't within the app's base URI, use a
custom AuthorizationMessageHandler class (recommended) or configure the
AuthorizationMessageHandler.

7 Note

In addition to the client app configuration for server API access, the server API must
also allow cross-origin requests (CORS) when the client and the server don't reside
at the same base address. For more information on server-side CORS configuration,
see the Cross-origin resource sharing (CORS) section later in this article.

In the following example:

AddHttpClient adds IHttpClientFactory and related services to the service collection


and configures a named HttpClient ( WebAPI ). HttpClient.BaseAddress is the base
address of the resource URI when sending requests. IHttpClientFactory is provided
by the Microsoft.Extensions.Http NuGet package.
BaseAddressAuthorizationMessageHandler is the DelegatingHandler used to
process access tokens. Access tokens are only added when the request URI is
within the app's base URI.
IHttpClientFactory.CreateClient creates and configures an HttpClient instance for
outgoing requests using the configuration that corresponds to the named
HttpClient ( WebAPI ).

In the following example, HttpClientFactoryServiceCollectionExtensions.AddHttpClient is


an extension in Microsoft.Extensions.Http. Add the package to an app that doesn't
already reference it.

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 .

C#

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient("WebAPI",
client => client.BaseAddress = new
Uri("https://www.example.com/base"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()


.CreateClient("WebAPI"));

For a hosted Blazor solution based on the Blazor WebAssembly project template,
request URIs are within the app's base URI by default. Therefore,
IWebAssemblyHostEnvironment.BaseAddress ( new
Uri(builder.HostEnvironment.BaseAddress) ) is assigned to the HttpClient.BaseAddress in
an app generated from the project template.

The configured HttpClient is used to make authorized requests using the try-catch
pattern:

razor

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http

...

protected override async Task OnInitializedAsync()


{
try
{
var examples =
await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

...
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}

Custom authentication request scenarios


The following scenarios demonstrate how to customize authentication requests and
how to obtain the login path from authentication options.

Customize the login process


Manage additional parameters to a login request with the following methods one or
more times on a new instance of InteractiveRequestOptions:

TryAddAdditionalParameter
TryRemoveAdditionalParameter
TryGetAdditionalParameter

In the following LoginDisplay component example, additional parameters are added to


the login request:

prompt is set to login : Forces the user to enter their credentials on that request,
negating single sign on.
loginHint is set to peter@contoso.com : Pre-fills the username/email address field
of the sign-in page for the user to peter@contoso.com . Apps often use this
parameter during re-authentication, having already extracted the username from a
previous sign in using the preferred_username claim.

Shared/LoginDisplay.razor :

C#

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation
<AuthorizeView>
<Authorized>
Hello, @context.User.Identity?.Name!
<button @onclick="BeginLogOut">Log out</button>
</Authorized>
<NotAuthorized>
<button @onclick="BeginLogIn">Log in</button>
</NotAuthorized>
</AuthorizeView>

@code{
public void BeginLogOut()
{
Navigation.NavigateToLogout("authentication/logout");
}

public void BeginLogIn()


{
InteractiveRequestOptions requestOptions =
new()
{
Interaction = InteractionType.SignIn,
ReturnUrl = Navigation.Uri,
};

requestOptions.TryAddAdditionalParameter("prompt", "login");
requestOptions.TryAddAdditionalParameter("loginHint",
"peter@contoso.com");

Navigation.NavigateToLogin("authentication/login", requestOptions);
}
}

For more information, see the following resources:

InteractiveRequestOptions
Popup request parameter list

Customize options before obtaining a token interactively


If an AccessTokenNotAvailableException occurs, manage additional parameters for a
new identity provider access token request with the following methods one or more
times on a new instance of InteractiveRequestOptions:

TryAddAdditionalParameter
TryRemoveAdditionalParameter
TryGetAdditionalParameter
In the following example that obtains JSON data via web API, additional parameters are
added to the redirect request if an access token isn't available
(AccessTokenNotAvailableException is thrown):

prompt is set to login : Forces the user to enter their credentials on that request,
negating single sign on.
loginHint is set to peter@contoso.com : Pre-fills the username/email address field

of the sign-in page for the user to peter@contoso.com . Apps often use this
parameter during re-authentication, having already extracted the username from a
previous sign in using the preferred_username claim.

C#

try
{
var examples = await Http.GetFromJsonAsync<ExampleType[]>
("ExampleAPIMethod");

...
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect(requestOptions => {
requestOptions.TryAddAdditionalParameter("prompt", "login");
requestOptions.TryAddAdditionalParameter("loginHint",
"peter@contoso.com");
});
}

The preceding example assumes that:

The presence of an @using / using statement for API in the


Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace.
HttpClient injected as Http .

For more information, see the following resources:

InteractiveRequestOptions
Redirect request parameter list

Customize options when using an IAccessTokenProvider


If obtaining a token fails when using an IAccessTokenProvider, manage additional
parameters for a new identity provider access token request with the following methods
one or more times on a new instance of InteractiveRequestOptions:
TryAddAdditionalParameter
TryRemoveAdditionalParameter
TryGetAdditionalParameter

In the following example that attempts to obtain an access token for the user, additional
parameters are added to the login request if the attempt to obtain a token fails when
TryGetToken is called:

prompt is set to login : Forces the user to enter their credentials on that request,
negating single sign on.
loginHint is set to peter@contoso.com : Pre-fills the username/email address field
of the sign-in page for the user to peter@contoso.com . Apps often use this
parameter during re-authentication, having already extracted the username from a
previous sign in using the preferred_username claim.

C#

var tokenResult = await TokenProvider.RequestAccessToken(


new AccessTokenRequestOptions
{
Scopes = new[] { ... }
});

if (!tokenResult.TryGetToken(out var token))


{
tokenResult.InteractionOptions.TryAddAdditionalParameter("prompt",
"login");
tokenResult.InteractionOptions.TryAddAdditionalParameter("loginHint",
"peter@contoso.com");

Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl,
accessTokenResult.InteractionOptions);
}

The preceding example assumes:

The presence of an @using / using statement for API in the


Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace.
IAccessTokenProvider injected as TokenProvider .

For more information, see the following resources:

InteractiveRequestOptions
Popup request parameter list

Logout with a custom return URL


The following example logs out the user and returns the user to the /goodbye endpoint:

C#

Navigation.NavigateToLogout("authentication/logout", "goodbye");

Obtain the login path from authentication options


Obtain the configured login path from RemoteAuthenticationOptions:

C#

var loginPath =

RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath;

The preceding example assumes:

The presence of an @using / using statement for API in the following namespaces:
Microsoft.AspNetCore.Components.WebAssembly.Authentication
Microsoft.Extensions.Options
IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>

> injected as RemoteAuthOptions .

Custom AuthorizationMessageHandler class


This guidance in this section is recommended for client apps that make outgoing requests
to URIs that aren't within the app's base URI.

In the following example, a custom class extends AuthorizationMessageHandler for use


as the DelegatingHandler for an HttpClient. ConfigureHandler configures this handler to
authorize outbound HTTP requests using an access token. The access token is only
attached if at least one of the authorized URLs is a base of the request URI
(HttpRequestMessage.RequestUri).

C#

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler


{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigation)
: base(provider, navigation)
{
ConfigureHandler(
authorizedUrls: new[] { "https://www.example.com/base" },
scopes: new[] { "example.read", "example.write" });
}
}

In the preceding code, the scopes example.read and example.write are generic
examples not meant to reflect valid scopes for any particular provider.

In Program.cs , CustomAuthorizationMessageHandler is registered as a transient service


and is configured as the DelegatingHandler for outgoing HttpResponseMessage
instances made by a named HttpClient.

In the following example, HttpClientFactoryServiceCollectionExtensions.AddHttpClient is


an extension in Microsoft.Extensions.Http. Add the package to an app that doesn't
already reference it.

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 .

C#

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

builder.Services.AddHttpClient("WebAPI",
client => client.BaseAddress = new
Uri("https://www.example.com/base"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

7 Note

In the preceding example, the CustomAuthorizationMessageHandler


DelegatingHandler is registered as a transient service for
AddHttpMessageHandler. Transient registration is recommended for
IHttpClientFactory, which manages its own DI scopes. For more information, see
the following resources:

Utility base component classes to manage a DI scope


Detect transient disposables in Blazor WebAssembly apps

For a hosted Blazor solution based on the Blazor WebAssembly project template,
IWebAssemblyHostEnvironment.BaseAddress ( new
Uri(builder.HostEnvironment.BaseAddress) ) is assigned to the HttpClient.BaseAddress

by default.

The configured HttpClient is used to make authorized requests using the try-catch
pattern. Where the client is created with CreateClient (Microsoft.Extensions.Http
package), the HttpClient is supplied instances that include access tokens when making
requests to the server API. If the request URI is a relative URI, as it is in the following
example ( ExampleAPIMethod ), it's combined with the BaseAddress when the client app
makes the request:

razor

@inject IHttpClientFactory ClientFactory

...

@code {
protected override async Task OnInitializedAsync()
{
try
{
var client = ClientFactory.CreateClient("WebAPI");

var examples =
await client.GetFromJsonAsync<ExampleType[]>
("ExampleAPIMethod");

...
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}

Configure AuthorizationMessageHandler
AuthorizationMessageHandler can be configured with authorized URLs, scopes, and a
return URL using the ConfigureHandler method. ConfigureHandler configures the
handler to authorize outbound HTTP requests using an access token. The access token
is only attached if at least one of the authorized URLs is a base of the request URI
(HttpRequestMessage.RequestUri). If the request URI is a relative URI, it's combined with
the BaseAddress.

In the following example, AuthorizationMessageHandler configures an HttpClient in


Program.cs :

C#

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddScoped(sp => new HttpClient(


sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new[] { "https://www.example.com/base" },
scopes: new[] { "example.read", "example.write" }))
{
BaseAddress = new Uri("https://www.example.com/base")
});

In the preceding code, the scopes example.read and example.write are generic
examples not meant to reflect valid scopes for any particular provider.

For a hosted Blazor solution based on the Blazor WebAssembly project template,
IWebAssemblyHostEnvironment.BaseAddress is assigned to the following by default:

The HttpClient.BaseAddress ( new Uri(builder.HostEnvironment.BaseAddress) ).


A URL of the authorizedUrls array.

Typed HttpClient
A typed client can be defined that handles all of the HTTP and token acquisition
concerns within a single class.

WeatherForecastClient.cs :

C#

using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient


{
private readonly HttpClient http;
private WeatherForecast[]? forecasts;

public WeatherForecastClient(HttpClient http)


{
this.http = http;
}

public async Task<WeatherForecast[]> GetForecastAsync()


{
try
{
forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
"WeatherForecast");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}

return forecasts ?? Array.Empty<WeatherForecast>();


}
}

In the preceding example, the WeatherForecast type is a static class that holds weather
forecast data. The placeholder {ASSEMBLY NAME} is the app's assembly name (for
example, using static BlazorSample.Data; ).

In the following example, HttpClientFactoryServiceCollectionExtensions.AddHttpClient is


an extension in Microsoft.Extensions.Http. Add the package to an app that doesn't
already reference it.

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 .

In Program.cs :

C#

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient<WeatherForecastClient>(
client => client.BaseAddress = new
Uri("https://www.example.com/base"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

For a hosted Blazor solution based on the Blazor WebAssembly project template,
IWebAssemblyHostEnvironment.BaseAddress ( new
Uri(builder.HostEnvironment.BaseAddress) ) is assigned to the HttpClient.BaseAddress

by default.

FetchData component ( Pages/FetchData.razor ):

razor

@inject WeatherForecastClient Client

...

protected override async Task OnInitializedAsync()


{
forecasts = await Client.GetForecastAsync();
}

Configure the HttpClient handler


The handler can be further configured with ConfigureHandler for outbound HTTP
requests.

In the following example, HttpClientFactoryServiceCollectionExtensions.AddHttpClient is


an extension in Microsoft.Extensions.Http. Add the package to an app that doesn't
already reference it.

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 .

In Program.cs :

C#

builder.Services.AddHttpClient<WeatherForecastClient>(
client => client.BaseAddress = new
Uri("https://www.example.com/base"))
.AddHttpMessageHandler(sp =>
sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new [] { "https://www.example.com/base" },
scopes: new[] { "example.read", "example.write" }));

In the preceding code, the scopes example.read and example.write are generic
examples not meant to reflect valid scopes for any particular provider.

For a hosted Blazor solution based on the Blazor WebAssembly project template,
IWebAssemblyHostEnvironment.BaseAddress is assigned to the following by default:

The HttpClient.BaseAddress ( new Uri(builder.HostEnvironment.BaseAddress) ).


A URL of the authorizedUrls array.

Unauthenticated or unauthorized web API


requests in an app with a secure default client
An app that ordinarily uses a secure default HttpClient can also make unauthenticated
or unauthorized web API requests by configuring a named HttpClient.

In the following example, HttpClientFactoryServiceCollectionExtensions.AddHttpClient is


an extension in Microsoft.Extensions.Http. Add the package to an app that doesn't
already reference it.

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 .

In Program.cs :

C#

builder.Services.AddHttpClient("WebAPI.NoAuthenticationClient",
client => client.BaseAddress = new Uri("https://www.example.com/base"));

For a hosted Blazor solution based on the Blazor WebAssembly project template,
IWebAssemblyHostEnvironment.BaseAddress ( new
Uri(builder.HostEnvironment.BaseAddress) ) is assigned to the HttpClient.BaseAddress
by default.
The preceding registration is in addition to the existing secure default HttpClient
registration.

A component creates the HttpClient from the IHttpClientFactory


(Microsoft.Extensions.Http package) to make unauthenticated or unauthorized
requests:

razor

@inject IHttpClientFactory ClientFactory

...

@code {
protected override async Task OnInitializedAsync()
{
var client =
ClientFactory.CreateClient("WebAPI.NoAuthenticationClient");

var examples = await client.GetFromJsonAsync<ExampleType[]>(


"ExampleNoAuthentication");

...
}
}

7 Note

The controller in the server API, ExampleNoAuthenticationController for the


preceding example, isn't marked with the [Authorize] attribute.

The decision whether to use a secure client or an insecure client as the default
HttpClient instance is up to the developer. One way to make this decision is to consider
the number of authenticated versus unauthenticated endpoints that the app contacts. If
the majority of the app's requests are to secure API endpoints, use the authenticated
HttpClient instance as the default. Otherwise, register the unauthenticated HttpClient
instance as the default.

An alternative approach to using the IHttpClientFactory is to create a typed client for


unauthenticated access to anonymous endpoints.

Request additional access tokens


Access tokens can be manually obtained by calling
IAccessTokenProvider.RequestAccessToken. In the following example, an additional
scope is required by an app for the default HttpClient. The Microsoft Authentication
Library (MSAL) example configures the scope with MsalProviderOptions :

In Program.cs :

C#

builder.Services.AddMsalAuthentication(options =>
{
...

options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE
1}");
options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE
2}");
}

The {CUSTOM SCOPE 1} and {CUSTOM SCOPE 2} placeholders in the preceding example are
custom scopes.

The IAccessTokenProvider.RequestAccessToken method provides an overload that allows


an app to provision an access token with a given set of scopes.

In a Razor component:

razor

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider

...

var tokenResult = await TokenProvider.RequestAccessToken(


new AccessTokenRequestOptions
{
Scopes = new[] { "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" }
});

if (tokenResult.TryGetToken(out var token))


{
...
}

The {CUSTOM SCOPE 1} and {CUSTOM SCOPE 2} placeholders in the preceding example are
custom scopes.

AccessTokenResult.TryGetToken returns:
true with the token for use.

false if the token isn't retrieved.

Cross-origin resource sharing (CORS)


When sending credentials (authorization cookies/headers) on CORS requests, the
Authorization header must be allowed by the CORS policy.

The following policy includes configuration for:

Request origins ( http://localhost:5000 , https://localhost:5001 ).


Any method (verb).
Content-Type and Authorization headers. To allow a custom header (for example,
x-custom-header ), list the header when calling WithHeaders.

Credentials set by client-side JavaScript code ( credentials property set to


include ).

C#

app.UseCors(policy =>
policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
.AllowAnyMethod()
.WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization,
"x-custom-header")
.AllowCredentials());

A hosted Blazor solution based on the Blazor WebAssembly project template uses the
same base address for the client and server apps. The client app's
HttpClient.BaseAddress is set to a URI of builder.HostEnvironment.BaseAddress by
default. CORS configuration is not required in the default configuration of a hosted
Blazor solution. Additional client apps that aren't hosted by the server project and don't
share the server app's base address do require CORS configuration in the server project.

For more information, see Enable Cross-Origin Requests (CORS) in ASP.NET Core and
the sample app's HTTP Request Tester component
( Components/HTTPRequestTester.razor ).

Handle token request errors


When a single-page application (SPA) authenticates a user using OpenID Connect
(OIDC), the authentication state is maintained locally within the SPA and in the Identity
Provider (IP) in the form of a session cookie that's set as a result of the user providing
their credentials.

The tokens that the IP emits for the user typically are valid for short periods of time,
about one hour normally, so the client app must regularly fetch new tokens. Otherwise,
the user would be logged-out after the granted tokens expire. In most cases, OIDC
clients are able to provision new tokens without requiring the user to authenticate again
thanks to the authentication state or "session" that is kept within the IP.

There are some cases in which the client can't get a token without user interaction, for
example, when for some reason the user explicitly logs out from the IP. This scenario
occurs if a user visits https://login.microsoftonline.com and logs out. In these
scenarios, the app doesn't know immediately that the user has logged out. Any token
that the client holds might no longer be valid. Also, the client isn't able to provision a
new token without user interaction after the current token expires.

These scenarios aren't specific to token-based authentication. They are part of the
nature of SPAs. An SPA using cookies also fails to call a server API if the authentication
cookie is removed.

When an app performs API calls to protected resources, you must be aware of the
following:

To provision a new access token to call the API, the user might be required to
authenticate again.
Even if the client has a token that seems to be valid, the call to the server might fail
because the token was revoked by the user.

When the app requests a token, there are two possible outcomes:

The request succeeds, and the app has a valid token.


The request fails, and the app must authenticate the user again to obtain a new
token.

When a token request fails, you need to decide whether you want to save any current
state before you perform a redirection. Several approaches exist to store state with
increasing levels of complexity:

Store the current page state in session storage. During the OnInitializedAsync
lifecycle method (OnInitializedAsync), check if state can be restored before
continuing.
Add a query string parameter and use that as a way to signal the app that it needs
to re-hydrate the previously saved state.
Add a query string parameter with a unique identifier to store data in session
storage without risking collisions with other items.

Save app state before an authentication


operation with session storage
The following example shows how to:

Preserve state before redirecting to the login page.


Recover the previous state after authentication using a query string parameter.

razor

...
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject IJSRuntime JS
@inject NavigationManager Navigation

<EditForm Model="User" @onsubmit="OnSaveAsync">


<label>User
<InputText @bind-Value="User.Name" />
</label>
<label>Last name
<InputText @bind-Value="User.LastName" />
</label>
</EditForm>

@code {
public class Profile
{
public string? Name { get; set; }
public string? LastName { get; set; }
}

public Profile User { get; set; } = new Profile();

protected override async Task OnInitializedAsync()


{
var currentQuery = new Uri(Navigation.Uri).Query;

if (currentQuery.Contains("state=resumeSavingProfile"))
{
User = await JS.InvokeAsync<Profile>("sessionStorage.getItem",
"resumeSavingProfile");
}
}

public async Task OnSaveAsync()


{
var http = new HttpClient();
http.BaseAddress = new Uri(Navigation.BaseUri);

var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile";

var tokenResult = await TokenProvider.RequestAccessToken(


new AccessTokenRequestOptions
{
ReturnUrl = resumeUri
});

if (tokenResult.TryGetToken(out var token))


{
http.DefaultRequestHeaders.Add("Authorization",
$"Bearer {token.Value}");
await http.PostAsJsonAsync("Save", User);
}
else
{
await JS.InvokeVoidAsync("sessionStorage.setItem",
"resumeSavingProfile", User);
Navigation.NavigateTo(tokenResult.InteractiveRequestUrl);
}
}
}

Save app state before an authentication


operation with session storage and a state
container
During an authentication operation, there are cases where you want to save the app
state before the browser is redirected to the IP. This can be the case when you're using a
state container and want to restore the state after the authentication succeeds. You can
use a custom authentication state object to preserve app-specific state or a reference to
it and restore that state after the authentication operation successfully completes. The
following example demonstrates the approach.

A state container class is created in the app with properties to hold the app's state
values. In the following example, the container is used to maintain the counter value of
the default Blazor project template's Counter component ( Pages/Counter.razor ).
Methods for serializing and deserializing the container are based on System.Text.Json.

C#

using System.Text.Json;

public class StateContainer


{
public int CounterValue { get; set; }

public string GetStateForLocalStorage()


{
return JsonSerializer.Serialize(this);
}

public void SetStateFromLocalStorage(string locallyStoredState)


{
var deserializedState =
JsonSerializer.Deserialize<StateContainer>(locallyStoredState);

CounterValue = deserializedState.CounterValue;
}
}

The Counter component uses the state container to maintain the currentCount value
outside of the component:

razor

@page "/counter"
@inject StateContainer State

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

protected override void OnInitialized()


{
if (State.CounterValue > 0)
{
currentCount = State.CounterValue;
}
}

private void IncrementCount()


{
currentCount++;
State.CounterValue = currentCount;
}
}

Create an ApplicationAuthenticationState from RemoteAuthenticationState. Provide an


Id property, which serves as an identifier for the locally-stored state.
ApplicationAuthenticationState.cs :

C#

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState


{
public string? Id { get; set; }
}

The Authentication component ( Pages/Authentication.razor ) saves and restores the


app's state using local session storage with the StateContainer serialization and
deserialization methods, GetStateForLocalStorage and SetStateFromLocalStorage :

razor

@page "/authentication/{action}"
@inject IJSRuntime JS
@inject StateContainer State
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorViewCore Action="@Action"

TAuthenticationState="ApplicationAuthenticationState"
AuthenticationState="AuthenticationState"
OnLogInSucceeded="RestoreState"
OnLogOutSucceeded="RestoreState" />

@code {
[Parameter]
public string? Action { get; set; }

public ApplicationAuthenticationState AuthenticationState { get; set; }


=
new ApplicationAuthenticationState();

protected override async Task OnInitializedAsync()


{
if
(RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
Action) ||

RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
Action))
{
AuthenticationState.Id = Guid.NewGuid().ToString();

await JS.InvokeVoidAsync("sessionStorage.setItem",
AuthenticationState.Id, State.GetStateForLocalStorage());
}
}

private async Task RestoreState(ApplicationAuthenticationState state)


{
if (state.Id != null)
{
var locallyStoredState = await JS.InvokeAsync<string>(
"sessionStorage.getItem", state.Id);

if (locallyStoredState != null)
{
State.SetStateFromLocalStorage(locallyStoredState);
await JS.InvokeVoidAsync("sessionStorage.removeItem",
state.Id);
}
}
}
}

This example uses Azure Active Directory (AAD) for authentication. In Program.cs :

The ApplicationAuthenticationState is configured as the Microsoft Authentication


Library (MSAL) RemoteAuthenticationState type.
The state container is registered in the service container.

C#

builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>
(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();

Customize app routes


By default, the Microsoft.AspNetCore.Components.WebAssembly.Authentication
library uses the routes shown in the following table for representing different
authentication states.

Route Purpose

authentication/login Triggers a sign-in operation.


Route Purpose

authentication/login- Handles the result of any sign-in operation.


callback

authentication/login- Displays error messages when the sign-in operation fails for some
failed reason.

authentication/logout Triggers a sign-out operation.

authentication/logout- Handles the result of a sign-out operation.


callback

authentication/logout- Displays error messages when the sign-out operation fails for
failed some reason.

authentication/logged-out Indicates that the user has successfully logout.

authentication/profile Triggers an operation to edit the user profile.

authentication/register Triggers an operation to register a new user.

The routes shown in the preceding table are configurable via


RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.Authenticatio
nPaths. When setting options to provide custom routes, confirm that the app has a
route that handles each path.

In the following example, all of the paths are prefixed with /security .

Authentication component ( Pages/Authentication.razor ):

razor

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code{
[Parameter]
public string? Action { get; set; }
}

In Program.cs :

C#

builder.Services.AddApiAuthorization(options => {
options.AuthenticationPaths.LogInPath = "security/login";
options.AuthenticationPaths.LogInCallbackPath = "security/login-
callback";
options.AuthenticationPaths.LogInFailedPath = "security/login-failed";
options.AuthenticationPaths.LogOutPath = "security/logout";
options.AuthenticationPaths.LogOutCallbackPath = "security/logout-
callback";
options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed";
options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out";
options.AuthenticationPaths.ProfilePath = "security/profile";
options.AuthenticationPaths.RegisterPath = "security/register";
});

If the requirement calls for completely different paths, set the routes as described
previously and render the RemoteAuthenticatorView with an explicit action parameter:

razor

@page "/register"

<RemoteAuthenticatorView Action="@RemoteAuthenticationActions.Register" />

You're allowed to break the UI into different pages if you choose to do so.

Customize the authentication user interface


RemoteAuthenticatorView includes a default set of UI fragments for each authentication
state. Each state can be customized by passing in a custom RenderFragment. To
customize the displayed text during the initial login process, can change the
RemoteAuthenticatorView as follows.

Authentication component ( Pages/Authentication.razor ):

razor

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action">
<LoggingIn>
You are about to be redirected to https://login.microsoftonline.com.
</LoggingIn>
</RemoteAuthenticatorView>

@code{
[Parameter]
public string? Action { get; set; }
}
The RemoteAuthenticatorView has one fragment that can be used per authentication
route shown in the following table.

Route Fragment

authentication/login <LoggingIn>

authentication/login-callback <CompletingLoggingIn>

authentication/login-failed <LogInFailed>

authentication/logout <LogOut>

authentication/logout-callback <CompletingLogOut>

authentication/logout-failed <LogOutFailed>

authentication/logged-out <LogOutSucceeded>

authentication/profile <UserProfile>

authentication/register <Registering>

Customize the user


Users bound to the app can be customized.

Customize the user with a payload claim


In the following example, the app's authenticated users receive an amr claim for each of
the user's authentication methods. The amr claim identifies how the subject of the token
was authenticated in Microsoft Identity Platform v1.0 payload claims. The example uses
a custom user account class based on RemoteUserAccount.

Create a class that extends the RemoteUserAccount class. The following example sets
the AuthenticationMethod property to the user's array of amr JSON property values.
AuthenticationMethod is populated automatically by the framework when the user is
authenticated.

C#

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount


{
[JsonPropertyName("amr")]
public string[]? AuthenticationMethod { get; set; }
}

Create a factory that extends AccountClaimsPrincipalFactory<TAccount> to create


claims from the user's authentication methods stored in
CustomUserAccount.AuthenticationMethod :

C#

using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory


: AccountClaimsPrincipalFactory<CustomUserAccount>
{
public CustomAccountFactory(NavigationManager navigation,
IAccessTokenProviderAccessor accessor) : base(accessor)
{
}

public override async ValueTask<ClaimsPrincipal> CreateUserAsync(


CustomUserAccount account, RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);

if (initialUser.Identity != null &&


initialUser.Identity.IsAuthenticated)
{
var userIdentity = (ClaimsIdentity)initialUser.Identity;

if (account.AuthenticationMethod is not null)


{
foreach (var value in account.AuthenticationMethod)
{
userIdentity.AddClaim(new Claim("amr", value));
}
}
}

return initialUser;
}
}

Register the CustomAccountFactory for the authentication provider in use. Any of the
following registrations are valid:

AddOidcAuthentication:
C#

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddOidcAuthentication<RemoteAuthenticationState,
CustomUserAccount>(options =>
{
...
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState,
CustomUserAccount, CustomAccountFactory>();

AddMsalAuthentication:

C#

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
CustomUserAccount>(options =>
{
...
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState,
CustomUserAccount, CustomAccountFactory>();

AddApiAuthorization:

C#

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddApiAuthorization<RemoteAuthenticationState,
CustomUserAccount>(options =>
{
...
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState,
CustomUserAccount, CustomAccountFactory>();

AAD security groups and roles with a custom user


account class
For an additional example that works with AAD security groups and AAD Administrator
Roles and a custom user account class, see ASP.NET Core Blazor WebAssembly with
Azure Active Directory groups and roles.

Prerendering with authentication


Prerendering content that requires authentication and authorization isn't currently
supported. After following the guidance in one of the Blazor WebAssembly security app
topics, use the following instructions to create an app that:

Prerenders paths for which authorization isn't required.


Doesn't prerender paths for which authorization is required.

For the Client project's Program.cs file, factor common service registrations into a
separate method (for example, create a ConfigureCommonServices method in the Client
project). Common services are those that the developer registers for use by both the
client and server projects.

C#

public static void ConfigureCommonServices(IServiceCollection services)


{
services.Add...;
}

Program.cs :

C#

var builder = WebAssemblyHostBuilder.CreateDefault(args);


...

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();

In the Server project's Program.cs file, register the following additional services and call
ConfigureCommonServices :

C#

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddRazorPages();
builder.Services.TryAddScoped<AuthenticationStateProvider,
ServerAuthenticationStateProvider>();

Client.Program.ConfigureCommonServices(services);

For more information on the Blazor framework server authentication provider


( ServerAuthenticationStateProvider ), see ASP.NET Core Blazor authentication and
authorization.

In the Server project's Pages/_Host.cshtml file, replace the Component Tag Helper
( <component ... /> ) with the following:

CSHTML

<div id="app">
@if (HttpContext.Request.Path.StartsWithSegments("/authentication"))
{
<component type="typeof({CLIENT APP ASSEMBLY NAME}.App)"
render-mode="WebAssembly" />
}
else
{
<component type="typeof({CLIENT APP ASSEMBLY NAME}.App)"
render-mode="WebAssemblyPrerendered" />
}
</div>

In the preceding example:

The placeholder {CLIENT APP ASSEMBLY NAME} is the client app's assembly name (for
example BlazorSample.Client ).
The conditional check for the /authentication path segment:
Avoids prerendering ( render-mode="WebAssembly" ) for authentication paths.
Prerenders ( render-mode="WebAssemblyPrerendered" ) for non-authentication
paths.

Options for hosted apps and third-party login


providers
When authenticating and authorizing a hosted Blazor WebAssembly app with a third-
party provider, there are several options available for authenticating the user. Which one
you choose depends on your scenario.

For more information, see Persist additional claims and tokens from external providers in
ASP.NET Core.

Authenticate users to only call protected third party APIs


Authenticate the user with a client-side OAuth flow against the third-party API provider:

C#

builder.services.AddOidcAuthentication(options => { ... });

In this scenario:

The server hosting the app doesn't play a role.


APIs on the server can't be protected.
The app can only call protected third-party APIs.

Authenticate users with a third-party provider and call


protected APIs on the host server and the third party
Configure Identity with a third-party login provider. Obtain the tokens required for
third-party API access and store them.

When a user logs in, Identity collects access and refresh tokens as part of the
authentication process. At that point, there are a couple of approaches available for
making API calls to third-party APIs.

Use a server access token to retrieve the third-party access token


Use the access token generated on the server to retrieve the third-party access token
from a server API endpoint. From there, use the third-party access token to call third-
party API resources directly from Identity on the client.

We don't recommend this approach. This approach requires treating the third-party
access token as if it were generated for a public client. In OAuth terms, the public app
doesn't have a client secret because it can't be trusted to store secrets safely, and the
access token is produced for a confidential client. A confidential client is a client that has
a client secret and is assumed to be able to safely store secrets.
The third-party access token might be granted additional scopes to perform
sensitive operations based on the fact that the third-party emitted the token for a
more trusted client.
Similarly, refresh tokens shouldn't be issued to a client that isn't trusted, as doing
so gives the client unlimited access unless other restrictions are put into place.

Make API calls from the client to the server API in order to call
third-party APIs

Make an API call from the client to the server API. From the server, retrieve the access
token for the third-party API resource and issue whatever call is necessary.

We recommend this approach. While this approach requires an extra network hop
through the server to call a third-party API, it ultimately results in a safer experience:

The server can store refresh tokens and ensure that the app doesn't lose access to
third-party resources.
The app can't leak access tokens from the server that might contain more sensitive
permissions.

Use OpenID Connect (OIDC) v2.0 endpoints


The authentication library and Blazor project templates use OpenID Connect (OIDC) v1.0
endpoints. To use a v2.0 endpoint, configure the JWT Bearer JwtBearerOptions.Authority
option. In the following example, AAD is configured for v2.0 by appending a v2.0
segment to the Authority property:

C#

using Microsoft.AspNetCore.Authentication.JwtBearer;

...

builder.Services.Configure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme,
options =>
{
options.Authority += "/v2.0";
});

Alternatively, the setting can be made in the app settings ( appsettings.json ) file:

JSON
{
"Local": {
"Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/",
...
}
}

If tacking on a segment to the authority isn't appropriate for the app's OIDC provider,
such as with non-AAD providers, set the Authority property directly. Either set the
property in JwtBearerOptions or in the app settings file ( appsettings.json ) with the
Authority key.

The list of claims in the ID token changes for v2.0 endpoints. For more information, see
Why update to Microsoft identity platform (v2.0)?.

Configure and use gRPC in components


To configure a Blazor WebAssembly app to use the ASP.NET Core gRPC framework:

Enable gRPC-Web on the server. For more information, see gRPC-Web in ASP.NET
Core gRPC apps.
Register gRPC services for the app's message handler. The following example
configures the app's authorization message handler to use the GreeterClient
service from the gRPC tutorial ( Program.cs ):

C#

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;
using {ASSEMBLY NAME}.Shared;

...

builder.Services.AddScoped(sp =>
{
var baseAddressMessageHandler =
sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
var grpcWebHandler =
new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
var channel =
GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress,
new GrpcChannelOptions { HttpHandler = grpcWebHandler });
return new Greeter.GreeterClient(channel);
});

The placeholder {ASSEMBLY NAME} is the app's assembly name (for example,
BlazorSample ). Place the .proto file in the Shared project of the hosted Blazor solution.

A component in the client app can make gRPC calls using the gRPC client
( Pages/Grpc.razor ):

razor

@page "/grpc"
@using Microsoft.AspNetCore.Authorization
@using {ASSEMBLY NAME}.Shared
@attribute [Authorize]
@inject Greeter.GreeterClient GreeterClient

<h1>Invoke gRPC service</h1>

<p>
<input @bind="name" placeholder="Type your name" />
<button @onclick="GetGreeting" class="btn btn-primary">Call gRPC
service</button>
</p>

Server response: <strong>@serverResponse</strong>

@code {
private string name = "Bert";
private string? serverResponse;

private async Task GetGreeting()


{
try
{
var request = new HelloRequest { Name = name };
var reply = await GreeterClient.SayHelloAsync(request);
serverResponse = reply.Message;
}
catch (Grpc.Core.RpcException ex)
when (ex.Status.DebugException is
AccessTokenNotAvailableException tokenEx)
{
tokenEx.Redirect();
}
}
}

The placeholder {ASSEMBLY NAME} is the app's assembly name (for example,
BlazorSample ). To use the Status.DebugException property, use Grpc.Net.Client
version 2.30.0 or later.

For more information, see gRPC-Web in ASP.NET Core gRPC apps.

Replace the AuthenticationService


implementation
The following subsections explain how to replace:

Any JavaScript AuthenticationService implementation.


The Microsoft Authentication Library for JavaScript ( MSAL.js ).

Replace any JavaScript AuthenticationService


implementation
Create a JavaScript library to handle your custom authentication details.

2 Warning

The guidance in this section is an implementation detail of the default


RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProvider
Options>. The TypeScript code in this section applies specifically to ASP.NET Core
7.0 and is subject to change without notice in upcoming releases of ASP.NET Core.

TypeScript

// .NET makes calls to an AuthenticationService object in the Window.


declare global {
interface Window { AuthenticationService: AuthenticationService }
}

export interface AuthenticationService {


// Init is called to initialize the AuthenticationService.
public static init(settings: UserManagerSettings &
AuthorizeServiceSettings, logger: any) : Promise<void>;

// Gets the currently authenticated user.


public static getUser() : Promise<{[key: string] : string }>;

// Tries to get an access token silently.


public static getAccessToken(options: AccessTokenRequestOptions) :
Promise<AccessTokenResult>;

// Tries to sign in the user or get an access token interactively.


public static signIn(context: AuthenticationContext) :
Promise<AuthenticationResult>;

// Handles the sign-in process when a redirect is used.


public static async completeSignIn(url: string) :
Promise<AuthenticationResult>;

// Signs the user out.


public static signOut(context: AuthenticationContext) :
Promise<AuthenticationResult>;

// Handles the signout callback when a redirect is used.


public static async completeSignOut(url: string) :
Promise<AuthenticationResult>;
}

// The rest of these interfaces match their C# definitions.

export interface AccessTokenRequestOptions {


scopes: string[];
returnUrl: string;
}

export interface AccessTokenResult {


status: AccessTokenResultStatus;
token?: AccessToken;
}

export interface AccessToken {


value: string;
expires: Date;
grantedScopes: string[];
}

export enum AccessTokenResultStatus {


Success = 'Success',
RequiresRedirect = 'RequiresRedirect'
}

export enum AuthenticationResultStatus {


Redirect = 'Redirect',
Success = 'Success',
Failure = 'Failure',
OperationCompleted = 'OperationCompleted'
};

export interface AuthenticationResult {


status: AuthenticationResultStatus;
state?: unknown;
message?: string;
}

export interface AuthenticationContext {


state?: unknown;
interactiveRequest: InteractiveAuthenticationRequest;
}
export interface InteractiveAuthenticationRequest {
scopes?: string[];
additionalRequestParameters?: { [key: string]: any };
};

You can import the library by removing the original <script> tag and adding a
<script> tag that loads the custom library. The following example demonstrates
replacing the default <script> tag with one that loads a library named
CustomAuthenticationService.js from the wwwroot/js folder.

In wwwroot/index.html before the Blazor script ( _framework/blazor.webassembly.js )


inside the closing </body> tag:

diff

- <script
src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationServic
e.js"></script>
+ <script src="js/CustomAuthenticationService.js"></script>

For more information, see AuthenticationService.ts in the dotnet/aspnetcore GitHub


repository .

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Replace the Microsoft Authentication Library for


JavaScript ( MSAL.js )
If an app requires a custom version of the Microsoft Authentication Library for JavaScript
(MSAL.js) , perform the following steps:

1. Confirm the system has the latest developer .NET SDK or obtain and install the
latest developer SDK from .NET Core SDK: Installers and Binaries . Configuration
of internal NuGet feeds isn't required for this scenario.
2. Set up the dotnet/aspnetcore GitHub repository for development following the
documentation at Build ASP.NET Core from Source . Fork and clone or download
a ZIP archive of the dotnet/aspnetcore GitHub repository .
3. Open the
src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json file

and set the desired version of @azure/msal-browser . For a list of released versions,
visit the @azure/msal-browser npm website and select the Versions tab.
4. Build the Authentication.Msal project in the
src/Components/WebAssembly/Authentication.Msal/src folder with the yarn build
command in a command shell.
5. If the app uses compressed assets (Brotli/Gzip), compress the
Interop/dist/Release/AuthenticationService.js file.
6. Copy the AuthenticationService.js file and compressed versions ( .br / .gz ) of the
file, if produced, from the Interop/dist/Release folder into the app's
publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal folder in

the app's published assets.

Pass custom provider options


Define a class for passing the data to the underlying JavaScript library.

) Important

The class's structure must match what the library expects when the JSON is
serialized with System.Text.Json.

The following example demonstrates a ProviderOptions class with JsonPropertyName


attributes matching a hypothetical custom provider library's expectations:

C#

public class ProviderOptions


{
public string? Authority { get; set; }
public string? MetadataUrl { get; set; }

[JsonPropertyName("client_id")]
public string? ClientId { get; set; }

public IList<string> DefaultScopes { get; } =


new List<string> { "openid", "profile" };

[JsonPropertyName("redirect_uri")]
public string? RedirectUri { get; set; }

[JsonPropertyName("post_logout_redirect_uri")]
public string? PostLogoutRedirectUri { get; set; }

[JsonPropertyName("response_type")]
public string? ResponseType { get; set; }

[JsonPropertyName("response_mode")]
public string? ResponseMode { get; set; }
}

Register the provider options within the DI system and configure the appropriate values:

C#

builder.Services.AddRemoteAuthentication<RemoteAuthenticationState,
RemoteUserAccount,
ProviderOptions>(options => {
options.Authority = "...";
options.MetadataUrl = "...";
options.ClientId = "...";
options.DefaultScopes = new List<string> { "openid", "profile",
"myApi" };
options.RedirectUri = "https://localhost:5001/authentication/login-
callback";
options.PostLogoutRedirectUri =
"https://localhost:5001/authentication/logout-callback";
options.ResponseType = "...";
options.ResponseMode = "...";
});

The preceding example sets redirect URIs with regular string literals. The following
alternatives are available:

TryCreate using IWebAssemblyHostEnvironment.BaseAddress:

C#

Uri.TryCreate(
$"{builder.HostEnvironment.BaseAddress}authentication/login-
callback",
UriKind.Absolute, out var redirectUri);
options.RedirectUri = redirectUri;

Host builder configuration:

C#

options.RedirectUri = builder.Configuration["RedirectUri"];
wwwroot/appsettings.json :

JSON

{
"RedirectUri": "https://localhost:5001/authentication/login-callback"
}

Additional resources
Use Graph API with ASP.NET Core Blazor WebAssembly
HttpClient and HttpRequestMessage with Fetch API request options
Azure Active Directory (AAD) groups,
Administrator Roles, and App Roles
Article • 03/17/2023

This article explains how to configure Blazor WebAssembly to use Azure Active Directory
groups and roles.

Azure Active Directory (AAD) provides several authorization approaches that can be
combined with ASP.NET Core Identity:

Groups
Security
Microsoft 365
Distribution
Roles
AAD Administrator Roles
App Roles

The guidance in this article applies to the Blazor WebAssembly AAD deployment
scenarios described in the following topics:

Standalone with Microsoft Accounts


Standalone with AAD
Hosted with AAD

The article's guidance provides instructions for client and server apps:

CLIENT: Standalone Blazor WebAssembly apps or the Client app of a hosted Blazor
solution.
SERVER: ASP.NET Core server API/web API apps or the Server app of a hosted
Blazor solution. You can ignore the SERVER guidance throughout the article for a
standalone Blazor WebAssembly app.

The examples in this article take advantage of recent .NET features released with
ASP.NET Core 6.0 or later. When using the examples in ASP.NET Core 5.0, minor
modifications are required. However, the text and code examples that pertain to
interacting with AAD and Microsoft Graph are the same for all versions of ASP.NET Core.

Prerequisite
The guidance in this article implements the Microsoft Graph API per the Graph SDK
guidance in Use Graph API with ASP.NET Core Blazor WebAssembly. Follow the Graph
SDK implementation guidance to configure the app and test it to confirm that the app
can obtain Graph API data for a test user account. Additionally, see the Graph API
article's security article cross-links to review Microsoft Graph security concepts.

When testing with the Graph SDK locally, we recommend using a new in-
private/incognito browser session for each test to prevent lingering cookies from
interfering with tests. For more information, see Secure an ASP.NET Core Blazor
WebAssembly standalone app with Azure Active Directory.

Scopes
To permit Microsoft Graph API calls for user profile, role assignment, and group
membership data:

A CLIENT app is configured with the User.Read scope


( https://graph.microsoft.com/User.Read ) in the Azure portal.
A SERVER app is configured with the GroupMember.Read.All scope
( https://graph.microsoft.com/GroupMember.Read.All ) in the Azure portal.

The preceding scopes are required in addition to the scopes required in AAD
deployment scenarios described by the topics listed earlier (Standalone with Microsoft
Accounts, Standalone with AAD, and Hosted with AAD).

For more information, see the Microsoft Graph permissions reference.

7 Note

The words "permission" and "scope" are used interchangeably in the Azure portal
and in various Microsoft and external documentation sets. This article uses the
word "scope" throughout for the permissions assigned to an app in the Azure
portal.

Group Membership Claims attribute


In the app's manifest in the Azure portal for CLIENT and SERVER apps, set the
groupMembershipClaims attribute to All . A value of All results in AAD sending all of
the security groups, distribution groups, and roles of the signed-in user in the well-
known IDs claim (wids):
1. Open the app's Azure portal registration.
2. Select Manage > Manifest in the sidebar.
3. Find the groupMembershipClaims attribute.
4. Set the value to All ( "groupMembershipClaims": "All" ).
5. Select the Save button if you made changes.

Custom user account


Assign users to AAD security groups and AAD Administrator Roles in the Azure portal.

The examples in this article:

Assume that a user is assigned to the AAD Billing Administrator role in the Azure
portal AAD tenant for authorization to access server API data.
Use authorization policies to control access within the CLIENT and SERVER apps.

In the CLIENT app, extend RemoteUserAccount to include properties for:

Roles : AAD App Roles array (covered in the App Roles section)

Wids : AAD Administrator Roles in well-known IDs claim (wids)


Oid : Immutable object identifier claim (oid) (uniquely identifies a user within and

across tenants)

CustomUserAccount.cs :

C#

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount


{
[JsonPropertyName("roles")]
public List<string>? Roles { get; set; }

[JsonPropertyName("wids")]
public List<string>? Wids { get; set; }

[JsonPropertyName("oid")]
public string? Oid { get; set; }
}

Add a package reference to the CLIENT app for Microsoft.Graph .

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 .

Add the Graph SDK utility classes and configuration in the Graph SDK guidance of the
Use Graph API with ASP.NET Core Blazor WebAssembly article. Specify the User.Read
scope for the access token as the article shows in its example wwwroot/appsettings.json
file.

Add the following custom user account factory to the CLIENT app. The custom user
factory is used to establish:

App Role claims ( appRole ) (covered in the App Roles section).


AAD Administrator Role claims ( directoryRole ).
Example user profile data claims for the user's mobile phone number
( mobilePhone ) and office location ( officeLocation ).
AAD Group claims ( directoryGroup ).
An ILogger ( logger ) for convenience in case you wish to log information or errors.

CustomAccountFactory.cs :

C#

using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;

public class CustomAccountFactory


: AccountClaimsPrincipalFactory<CustomUserAccount>
{
private readonly ILogger<CustomAccountFactory> logger;
private readonly IServiceProvider serviceProvider;

public CustomAccountFactory(IAccessTokenProviderAccessor accessor,


IServiceProvider serviceProvider,
ILogger<CustomAccountFactory> logger)
: base(accessor)
{
this.serviceProvider = serviceProvider;
this.logger = logger;
}

public override async ValueTask<ClaimsPrincipal> CreateUserAsync(


CustomUserAccount account,
RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity is not null &&
initialUser.Identity.IsAuthenticated)
{
var userIdentity = initialUser.Identity as ClaimsIdentity;

if (userIdentity is not null)


{
account?.Roles?.ForEach((role) =>
{
userIdentity.AddClaim(new Claim("appRole", role));
});

account?.Wids?.ForEach((wid) =>
{
userIdentity.AddClaim(new Claim("directoryRole", wid));
});

try
{
var client = ActivatorUtilities
.CreateInstance<GraphServiceClient>
(serviceProvider);
var request = client.Me.Request();
var user = await request.GetAsync();

if (user is not null)


{
userIdentity.AddClaim(new Claim("mobilephone",
user.MobilePhone ?? "(000) 000-0000"));
userIdentity.AddClaim(new Claim("officelocation",
user.OfficeLocation ?? "Not set"));
}

var requestMemberOf =
client.Users[account?.Oid].MemberOf;
var memberships = await
requestMemberOf.Request().GetAsync();

if (memberships is not null)


{
foreach (var entry in memberships)
{
if (entry.ODataType == "#microsoft.graph.group")
{
userIdentity.AddClaim(
new Claim("directoryGroup", entry.Id));
}
}
}
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}

return initialUser;
}
}

The preceding code doesn't include transitive memberships. If the app requires direct
and transitive group membership claims, replace the MemberOf property
( IUserMemberOfCollectionWithReferencesRequestBuilder ) with TransitiveMemberOf
( IUserTransitiveMemberOfCollectionWithReferencesRequestBuilder ).

The preceding code ignores group membership claims ( groups ) that are AAD
Administrator Roles ( #microsoft.graph.directoryRole type) because the GUID values
returned by the Microsoft identity platform are AAD Administrator Role entity IDs and
not Role Template IDs. Entity IDs aren't stable across tenants in Microsoft identity
platform and shouldn't be used to create authorization policies for users in apps. Always
use Role Template IDs for AAD Administrator Roles provided by wids claims.

In the CLIENT app, configure the MSAL authentication to use the custom user account
factory.

Confirm that the Program.cs file uses the


Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace:

C#

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

Update the AddMsalAuthentication call to the following. Note that the Blazor
framework's RemoteUserAccount is replaced by the app's CustomUserAccount for the
MSAL authentication and account claims principal factory:

C#

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
CustomUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState,
CustomUserAccount,
CustomAccountFactory>();
Confirm the presence of the Graph SDK code described by the Use Graph API with
ASP.NET Core Blazor WebAssembly article and that the wwwroot/appsettings.json
configuration is correct per the Graph SDK guidance:

C#

var baseUrl = string.Join("/",


builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"],
builder.Configuration.GetSection("MicrosoftGraph")["Version"]);
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
.Get<List<string>>();

builder.Services.AddGraphClient(baseUrl, scopes);

wwwroot/appsettings.json :

JSON

"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com",
"Version: "v1.0",
"Scopes": [
"user.read"
]
}

Authorization configuration
In the CLIENT app, create a policy for each App Role, AAD Administrator Role, or
security group in Program.cs . The following example creates a policy for the AAD Billing
Administrator role:

C#

builder.Services.AddAuthorizationCore(options =>
{
options.AddPolicy("BillingAdministrator", policy =>
policy.RequireClaim("directoryRole",
"b0f54661-2d74-4c50-afa3-1ec803f12efe"));
});

For the complete list of IDs for AAD Administrator Roles, see Role template IDs in the
Azure documentation. For more information on authorization policies, see Policy-based
authorization in ASP.NET Core.
In the following examples, the CLIENT app uses the preceding policy to authorize the
user.

The AuthorizeView component works with the policy:

razor

<AuthorizeView Policy="BillingAdministrator">
<Authorized>
<p>
The user is in the 'Billing Administrator' AAD Administrator
Role
and can see this content.
</p>
</Authorized>
<NotAuthorized>
<p>
The user is NOT in the 'Billing Administrator' role and sees
this
content.
</p>
</NotAuthorized>
</AuthorizeView>

Access to an entire component can be based on the policy using an [Authorize] attribute
directive (AuthorizeAttribute):

razor

@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "BillingAdministrator")]

If the user isn't authorized, they're redirected to the AAD sign-in page.

A policy check can also be performed in code with procedural logic.

Pages/CheckPolicy.razor :

razor

@page "/checkpolicy"
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

<h1>Check Policy</h1>

<p>This component checks a policy in code.</p>


<button @onclick="CheckPolicy">Check 'BillingAdministrator' policy</button>

<p>Policy Message: @policyMessage</p>

@code {
private string policyMessage = "Check hasn't been made yet.";

[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }

private async Task CheckPolicy()


{
var user = (await authenticationStateTask).User;

if ((await AuthorizationService.AuthorizeAsync(user,
"BillingAdministrator")).Succeeded)
{
policyMessage = "Yes! The 'BillingAdministrator' policy is
met.";
}
else
{
policyMessage = "No! 'BillingAdministrator' policy is NOT met.";
}
}
}

Authorize server API/web API access


A SERVER API app can authorize users to access secure API endpoints with authorization
policies for security groups, AAD Administrator Roles, and App Roles when an access
token contains groups , wids , and role claims. The following example creates a policy
for the AAD Billing Administrator role in Program.cs using the wids (well-known
IDs/Role Template IDs) claims:

C#

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("BillingAdministrator", policy =>
policy.RequireClaim("wids", "b0f54661-2d74-4c50-afa3-
1ec803f12efe"));
});

For the complete list of IDs for AAD Administrator Roles, see Role template IDs in the
Azure documentation. For more information on authorization policies, see Policy-based
authorization in ASP.NET Core.
Access to a controller in the SERVER app can be based on using an [Authorize] attribute
with the name of the policy (API documentation: AuthorizeAttribute).

The following example limits access to billing data from the BillingDataController to
Azure Billing Administrators with a policy name of BillingAdministrator :

C#

...
using Microsoft.AspNetCore.Authorization;

[Authorize(Policy = "BillingAdministrator")]
[ApiController]
[Route("[controller]")]
public class BillingDataController : ControllerBase
{
...
}

For more information, see Policy-based authorization in ASP.NET Core.

App Roles
To configure the app in the Azure portal to provide App Roles membership claims, see
How to: Add app roles in your application and receive them in the token in the Azure
documentation.

The following example assumes that the CLIENT and SERVER apps are configured with
two roles, and the roles are assigned to a test user:

Admin

Developer

7 Note

When developing a hosted Blazor WebAssembly app or a client-server pair of


standalone apps (a standalone Blazor WebAssembly app and an ASP.NET Core
server API/web API app), the appRoles manifest property of both the client and the
server Azure portal app registrations must include the same configured roles. After
establishing the roles in the client app's manifest, copy them in their entirety to the
server app's manifest. If you don't mirror the manifest appRoles between the client
and server app registrations, role claims aren't established for authenticated users
of the server API/web API, even if their access token has the correct entries in the
role claims.
Although you can't assign roles to groups without an Azure AD Premium account, you
can assign roles to users and receive a role claim for users with a standard Azure
account. The guidance in this section doesn't require an AAD Premium account.

If you have a Premium tier Azure account, Manage > App roles appears in the Azure
portal app registration sidebar. Follow the guidance in How to: Add app roles in your
application and receive them in the token to configure the app's roles.

If you don't have a Premium tier Azure account, edit the app's manifest in the Azure
portal. Follow the guidance in Application roles: Roles using Azure AD App Roles:
Implementation to establish the app's roles manually in the appRoles entry of the
manifest file. Save the changes to the file.

The following is an example appRoles entry that creates Admin and Developer roles.
These example roles are used later in this section's example at the component level to
implement access restrictions:

JSON

"appRoles": [
{
"allowedMemberTypes": [
"User"
],
"description": "Administrators manage developers.",
"displayName": "Admin",
"id": "584e483a-7101-404b-9bb1-83bf9463e335",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "Admin"
},
{
"allowedMemberTypes": [
"User"
],
"description": "Developers write code.",
"displayName": "Developer",
"id": "82770d35-2a93-4182-b3f5-3d7bfe9dfe46",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "Developer"
}
],
7 Note

You can generate GUIDs with an online GUID generator program (Google search
result for "guid generator") .

To assign a role to a user (or group if you have a Premium tier Azure account):

1. Navigate to Enterprise applications in the AAD area of the Azure portal.


2. Select the app. Select Manage > Users and groups from the sidebar.
3. Select the checkbox for one or more user accounts.
4. From the menu above the list of users, select Edit assignment.
5. For the Select a role entry, select None selected.
6. Choose a role from the list and use the Select button to select it.
7. Use the Assign button at the bottom of the screen to assign the role.

Multiple roles are assigned in the Azure portal by re-adding a user for each additional
role assignment. Use the Add user/group button at the top of the list of users to re-add
a user. Use the preceding steps to assign another role to the user. You can repeat this
process as many times as needed to add additional roles to a user (or group).

The CustomAccountFactory shown in the Custom user account section is set up to act on
a role claim with a JSON array value. Add and register the CustomAccountFactory in the
CLIENT app as shown in the Custom user account section. There's no need to provide
code to remove the original role claim because it's automatically removed by the
framework.

In Program.cs of a CLIENT app, specify the claim named " appRole " as the role claim for
ClaimsPrincipal.IsInRole checks:

C#

builder.Services.AddMsalAuthentication(options =>
{
...

options.UserOptions.RoleClaim = "appRole";
});

7 Note

If you prefer to use the directoryRoles claim (ADD Administrator Roles), assign
" directoryRoles " to the RemoteAuthenticationUserOptions.RoleClaim.
In Program.cs of a SERVER app, specify the claim named
" http://schemas.microsoft.com/ws/2008/06/identity/claims/role " as the role claim for
ClaimsPrincipal.IsInRole checks:

C#

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
Configuration.Bind("AzureAd", options);
options.TokenValidationParameters.RoleClaimType =
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
},
options => { Configuration.Bind("AzureAd", options); });

7 Note

When a single authentication scheme is registered, the authentication scheme is


automatically used as the app's default scheme, and it isn't necessary to state the
scheme to AddAuthentication or via AuthenticationOptions. For more
information, see Overview of ASP.NET Core Authentication and the ASP.NET Core
announcement (aspnet/Announcements #490) .

7 Note

If you prefer to use the wids claim (ADD Administrator Roles), assign " wids " to the
TokenValidationParameters.RoleClaimType.

After you've completed the preceding steps to create and assign roles to users (or
groups if you have a Premium tier Azure account) and implemented the
CustomAccountFactory with the Graph SDK, as explained earlier in this article and in Use

Graph API with ASP.NET Core Blazor WebAssembly, you should see an appRole claim for
each assigned role that a signed-in user is assigned (or roles assigned to groups that
they are members of). Run the app with a test user to confirm the claim(s) are present as
expected. When testing with the Graph SDK locally, we recommend using a new in-
private/incognito browser session for each test to prevent lingering cookies from
interfering with tests. For more information, see Secure an ASP.NET Core Blazor
WebAssembly standalone app with Azure Active Directory.

Component authorization approaches are functional at this point. Any of the


authorization mechanisms in components of the CLIENT app can use the Admin role to
authorize the user:

AuthorizeView component

razor

<AuthorizeView Roles="Admin">

[Authorize] attribute directive (AuthorizeAttribute)

razor

@attribute [Authorize(Roles = "Admin")]

Procedural logic

C#

var authState = await


AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;

if (user.IsInRole("Admin")) { ... }

Multiple role tests are supported:

Require that the user be in either the Admin or Developer role with the
AuthorizeView component:

razor

<AuthorizeView Roles="Admin, Developer">


...
</AuthorizeView>

Require that the user be in both the Admin and Developer roles with the
AuthorizeView component:

razor

<AuthorizeView Roles="Admin">
<AuthorizeView Roles="Developer">
...
</AuthorizeView>
</AuthorizeView>
Require that the user be in either the Admin or Developer role with the
[Authorize] attribute:

razor

@attribute [Authorize(Roles = "Admin, Developer")]

Require that the user be in both the Admin and Developer roles with the
[Authorize] attribute:

razor

@attribute [Authorize(Roles = "Admin")]


@attribute [Authorize(Roles = "Developer")]

Require that the user be in either the Admin or Developer role with procedural
code:

razor

@code {
private async Task DoSomething()
{
var authState = await AuthenticationStateProvider
.GetAuthenticationStateAsync();
var user = authState.User;

if (user.IsInRole("Admin") || user.IsInRole("Developer"))
{
...
}
else
{
...
}
}
}

Require that the user be in both the Admin and Developer roles with procedural
code by changing the conditional OR (||) to a conditional AND (&&) in the
preceding example:

C#

if (user.IsInRole("Admin") && user.IsInRole("Developer"))


Any of the authorization mechanisms in controllers of the SERVER app can use the
Admin role to authorize the user:

[Authorize] attribute directive (AuthorizeAttribute)

C#

[Authorize(Roles = "Admin")]

Procedural logic

C#

if (User.IsInRole("Admin")) { ... }

Multiple role tests are supported:

Require that the user be in either the Admin or Developer role with the
[Authorize] attribute:

C#

[Authorize(Roles = "Admin, Developer")]

Require that the user be in both the Admin and Developer roles with the
[Authorize] attribute:

C#

[Authorize(Roles = "Admin")]
[Authorize(Roles = "Developer")]

Require that the user be in either the Admin or Developer role with procedural
code:

C#

static readonly string[] scopeRequiredByApi = new string[] {


"API.Access" };

...

[HttpGet]
public IEnumerable<ReturnType> Get()
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

if (User.IsInRole("Admin") || User.IsInRole("Developer"))
{
...
}
else
{
...
}

return ...
}

Require that the user be in both the Admin and Developer roles with procedural
code by changing the conditional OR (||) to a conditional AND (&&) in the
preceding example:

C#

if (User.IsInRole("Admin") && User.IsInRole("Developer"))

Because .NET string comparisons are case-sensitive by default, matching role names is
also case-sensitive. For example, Admin (uppercase A ) is not treated as the same role as
admin (lowercase a ).

Pascal case is typically used for role names (for example, BillingAdministrator ), but the
use of Pascal case isn't a strict requirement. Different casing schemes, such as camel
case, kebab case, and snake case, are permitted. Using spaces in role names is also
unusual but permitted. For example, billing administrator is an unusual role name
format in .NET apps but valid.

Additional resources
Role template IDs (Azure documentation)
groupMembershipClaims attribute (Azure documentation)
How to: Add app roles in your application and receive them in the token (Azure
documentation)
Application roles (Azure documentation)
Claims-based authorization in ASP.NET Core
Role-based authorization in ASP.NET Core
ASP.NET Core Blazor authentication and authorization
Use Graph API with ASP.NET Core Blazor
WebAssembly
Article • 03/24/2023

This article explains how to use Microsoft Graph API in Blazor WebAssembly apps, which
is a RESTful web API that enables apps to access Microsoft Cloud service resources.

Two approaches are available for directly interacting with Microsoft Graph in Blazor
apps:

Graph SDK: The Microsoft Graph SDKs are designed to simplify building high-
quality, efficient, and resilient applications that access Microsoft Graph. Select the
Graph SDK button at the top of this article to adopt this approach.

Named HttpClient with Graph API: A named HttpClient can issue web API
requests to directly to Graph API. Select the Named HttpClient with Graph API
button at the top of this article to adopt this approach.

The guidance in this article isn't meant to replace the primary Microsoft Graph
documentation and additional Azure security guidance in other Microsoft
documentation sets. Assess the security guidance in the Additional resources section of
this article before implementing Microsoft Graph in a production environment. Follow
all of Microsoft's best practices to limit the attack surface area of your apps.

) Important

The scenarios described in this article apply to using Azure Active Directory (AAD)
as the identity provider, not AAD B2C. Using Microsoft Graph with a client-side
Blazor WebAssembly app and the AAD B2C identity provider isn't supported at this
time.

Using a hosted Blazor WebAssembly app is supported, where the Server app uses the
Graph SDK/API to provide Graph data to the Client app via web API. For more
information, see the Hosted Blazor WebAssembly solutions section of this article.

The examples in this article take advantage of recent .NET features released with
ASP.NET Core 6.0 or later. When using the examples in ASP.NET Core 5.0 or earlier,
minor modifications are required. However, the text and code examples that pertain to
interacting with Microsoft Graph are the same for all versions of ASP.NET Core.
The following guidance applies to Microsoft Graph v4. If you're upgrading an app from
SDK v4 to v5, see the Microsoft Graph .NET SDK v5 changelog and upgrade guide .

The Microsoft Graph SDK for use in Blazor apps is called the Microsoft Graph .NET Client
Library.

The Graph SDK examples require the following package references in the standalone
Blazor WebAssembly app or the Client app of a hosted Blazor WebAssembly solution:

Microsoft.Extensions.Http
Microsoft.Graph

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 .

After adding the Microsoft Graph API scopes in the AAD area of the Azure portal, add
the following app settings configuration to the wwwroot/appsettings.json file, which
includes the Graph base URL with Graph version and scopes. In the following example,
the User.Read scope is specified for the examples in later sections of this article.

JSON

"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": [
"user.read"
]
}

Add the following GraphClientExtensions class to the standalone app or Client app of a
hosted Blazor WebAssembly solution. The scopes are provided to the Scopes property
of the AccessTokenRequestOptions in the AuthenticateRequestAsync method. The
IHttpProvider.OverallTimeout is extended from the default value of 100 seconds to 300
seconds to give the HttpClient more time to receive a response from Microsoft Graph.

When an access token isn't obtained, the following code doesn't set a Bearer
authorization header for Graph requests.

GraphClientExtensions.cs :

C#
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;

internal static class GraphClientExtensions


{
public static IServiceCollection AddGraphClient(
this IServiceCollection services, string? baseUrl, List<string>?
scopes)
{
if (string.IsNullOrEmpty(baseUrl) || scopes.IsNullOrEmpty())
{
return services;
}

services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>
(
options =>
{
scopes?.ForEach((scope) =>
{

options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
});
});

services.AddScoped<IAuthenticationProvider,
GraphAuthenticationProvider>();

services.AddScoped<IHttpProvider, HttpClientHttpProvider>(sp =>


new HttpClientHttpProvider(new HttpClient()));

services.AddScoped(sp =>
{
return new GraphServiceClient(
baseUrl,
sp.GetRequiredService<IAuthenticationProvider>(),
sp.GetRequiredService<IHttpProvider>());
});

return services;
}

private class GraphAuthenticationProvider : IAuthenticationProvider


{
private readonly IConfiguration config;

public GraphAuthenticationProvider(IAccessTokenProvider
tokenProvider,
IConfiguration config)
{
TokenProvider = tokenProvider;
this.config = config;
}

public IAccessTokenProvider TokenProvider { get; }

public async Task AuthenticateRequestAsync(HttpRequestMessage


request)
{
var result = await TokenProvider.RequestAccessToken(
new AccessTokenRequestOptions()
{
Scopes =
config.GetSection("MicrosoftGraph:Scopes").Get<string[]>()
});

if (result.TryGetToken(out var token))


{
request.Headers.Authorization ??= new
AuthenticationHeaderValue(
"Bearer", token.Value);
}
}
}

private class HttpClientHttpProvider : IHttpProvider


{
private readonly HttpClient client;

public HttpClientHttpProvider(HttpClient client)


{
this.client = client;
}

public ISerializer Serializer { get; } = new Serializer();

public TimeSpan OverallTimeout { get; set; } =


TimeSpan.FromSeconds(300);

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage


request)
{
return client.SendAsync(request);
}

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage


request,
HttpCompletionOption completionOption,
CancellationToken cancellationToken)
{
return client.SendAsync(request, completionOption,
cancellationToken);
}

public void Dispose()


{
}
}
}

In Program.cs , add the Graph client services and configuration with the AddGraphClient
extension method:

C#

var baseUrl = builder.Configuration


.GetSection("MicrosoftGraph")["BaseUrl"];
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
.Get<List<string>>();

builder.Services.AddGraphClient(baseUrl, scopes);

Call Graph API from a component using the


Graph SDK
The following GraphExample component uses an injected GraphServiceClient to obtain
the user's AAD profile data and display their mobile phone number. For any test user
that you create in AAD, make sure that you give the user's AAD profile a mobile phone
number in the Azure portal.

Pages/GraphExample.razor :

razor

@page "/graph-example"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client

<h1>Microsoft Graph Component Example</h1>

@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
<p>Mobile Phone: @user.MobilePhone</p>
}

@code {
private Microsoft.Graph.User? user;

protected override async Task OnInitializedAsync()


{
var request = Client.Me.Request();
user = await request.GetAsync();
}
}

When testing with the Graph SDK locally, we recommend using a new in-
private/incognito browser session for each test to prevent lingering cookies from
interfering with tests. For more information, see Secure an ASP.NET Core Blazor
WebAssembly standalone app with Azure Active Directory.

Customize user claims using the Graph SDK


In the following example, the app creates mobile phone number and office location
claims for a user from their AAD user profile's data. The app must have the User.Read
Graph API scope configured in AAD. Any test users for this scenario must have a mobile
phone number and office location in their AAD profile, which can be added via the
Azure portal.

In the following custom user account factory:

An ILogger ( logger ) is included for convenience in case you wish to log


information or errors in the CreateUserAsync method.
In the event that an AccessTokenNotAvailableException is thrown, the user is
redirected to the identity provider to sign into their account. Additional or different
actions can be taken when requesting an access token fails. For example, the app
can log the AccessTokenNotAvailableException and create a support ticket for
further investigation.
The framework's RemoteUserAccount represents the user's account. If the app
requires a custom user account class that extends RemoteUserAccount, swap your
custom user account class for RemoteUserAccount in the following code.

CustomAccountFactory.cs :

C#

using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;

public class CustomAccountFactory


: AccountClaimsPrincipalFactory<RemoteUserAccount>
{
private readonly ILogger<CustomAccountFactory> logger;
private readonly IServiceProvider serviceProvider;

public CustomAccountFactory(IAccessTokenProviderAccessor accessor,


IServiceProvider serviceProvider,
ILogger<CustomAccountFactory> logger)
: base(accessor)
{
this.serviceProvider = serviceProvider;
this.logger = logger;
}

public override async ValueTask<ClaimsPrincipal> CreateUserAsync(


RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);

if (initialUser.Identity is not null &&


initialUser.Identity.IsAuthenticated)
{
var userIdentity = initialUser.Identity as ClaimsIdentity;

if (userIdentity is not null)


{
try
{
var client = ActivatorUtilities
.CreateInstance<GraphServiceClient>
(serviceProvider);
var request = client.Me.Request();
var user = await request.GetAsync();

if (user is not null)


{
userIdentity.AddClaim(new Claim("mobilephone",
user.MobilePhone ?? "(000) 000-0000"));
userIdentity.AddClaim(new Claim("officelocation",
user.OfficeLocation ?? "Not set"));
}
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}

return initialUser;
}
}

Configure the MSAL authentication to use the custom user account factory.

Confirm that the Program.cs file uses the


Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace:
C#

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

The example in this section builds on the approach of reading the base URL with version
and scopes from app configuration via the MicrosoftGraph section in
wwwroot/appsettings.json file. The following lines should already be present in
Program.cs from following the guidance earlier in this article:

C#

var baseUrl = string.Join("/",


builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"];
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
.Get<List<string>>();

builder.Services.AddGraphClient(baseUrl, scopes);

In Program.cs , find the call to the AddMsalAuthentication extension method. Update the
code to the following, which includes a call to AddAccountClaimsPrincipalFactory that
adds an account claims principal factory with the CustomAccountFactory .

If the app uses a custom user account class that extends RemoteUserAccount, swap the
custom user account class for RemoteUserAccount in the following code.

C#

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
RemoteUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState,
RemoteUserAccount,
CustomAccountFactory>();

You can use the following UserClaims component to study the user's claims after the
user authenticates with AAD:

Pages/UserClaims.razor :

razor

@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider
@attribute [Authorize]

<h1>User Claims</h1>

@if (claims.Any())
{
<ul>
@foreach (var claim in claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
}
else
{
<p>No claims found.</p>
}

@code {
private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

protected override async Task OnInitializedAsync()


{
var authState = await AuthenticationStateProvider
.GetAuthenticationStateAsync();
var user = authState.User;

claims = user.Claims;
}
}

When testing with the Graph SDK locally, we recommend using a new in-
private/incognito browser session for each test to prevent lingering cookies from
interfering with tests. For more information, see Secure an ASP.NET Core Blazor
WebAssembly standalone app with Azure Active Directory.

Hosted Blazor WebAssembly solutions


The examples in this article pertain to using the Graph SDK or a named HttpClient with
Graph API directly from a standalone Blazor WebAssembly app or directly from the
Client app of a hosted Blazor WebAssembly solution. An additional scenario that isn't
covered by this article is for a Client app of a hosted solution to call the Server app of
the solution via web API, and then the the Server app uses the Graph SDK/API to call
Microsoft Graph and return data to the Client app. Although this is a supported
approach, it isn't covered by this article. If you wish to adopt this approach:
Follow the guidance in Call a web API from an ASP.NET Core Blazor app for the
web API aspects on issuing requests to the Server app from the Client app and
returning data to the Client app.
Follow the guidance in the primary Microsoft Graph documentation to use the
Graph SDK with a typical ASP.NET Core app, which in this scenario is the Server
app of the solution. If you use the Blazor WebAssembly project template to the
create the hosted Blazor WebAssembly solution (ASP.NET Core Hosted/ -h|--
hosted ) with organizational authorization (single organization/ SingleOrg or

multiple organization/ MultiOrg ) and the Microsoft Graph option (Microsoft


identity platform > Connected Services > Add Microsoft Graph permissions in
Visual Studio or the --calls-graph option with the .NET CLI dotnet new
command), the Server app of the solution is configured to use the Graph SDK
when the solution is created from the project template.

Additional resources

General guidance
Microsoft Graph documentation
Microsoft Graph sample Blazor WebAssembly app : This sample demonstrates
how to use the Microsoft Graph .NET SDK to access data in Office 365 from Blazor
WebAssembly apps.
Build .NET apps with Microsoft Graph tutorial and Microsoft Graph sample
ASP.NET Core app : These resources are most appropriate for hosted Blazor
WebAssembly solutions, where the Server app is configured to access Microsoft
Graph as a typical ASP.NET Core app on behalf of the Client app. The Client app
uses web API to make requests to the Server app for Graph data. Although these
resources don't directly apply to calling Graph from client-side Blazor
WebAssembly apps, the AAD app configuration and Microsoft Graph coding
practices in the linked resources are relevant for standalone Blazor WebAssembly
apps and should be consulted for general best practices.

Security guidance
Microsoft Graph auth overview
Overview of Microsoft Graph permissions
Microsoft Graph permissions reference
Enhance security with the principle of least privilege
Microsoft Security Best Practices: Securing privileged access
Azure privilege escalation articles on the Internet (Google search result)
Enforce a Content Security Policy for
ASP.NET Core Blazor
Article • 05/19/2023

This article explains how to use a Content Security Policy (CSP) with ASP.NET Core
Blazor apps to help protect against Cross-Site Scripting (XSS) attacks.

Cross-Site Scripting (XSS) is a security vulnerability where an attacker places one or


more malicious client-side scripts into an app's rendered content. A CSP helps protect
against XSS attacks by informing the browser of valid:

Sources for loaded content, including scripts, stylesheets, images, and plugins.
Actions taken by a page, specifying permitted URL targets of forms.

To apply a CSP to an app, the developer specifies several CSP content security directives
in one or more Content-Security-Policy headers or <meta> tags. For guidance on
applying a CSP to an app in C# code at startup, see ASP.NET Core Blazor startup.

Policies are evaluated by the browser while a page is loading. The browser inspects the
page's sources and determines if they meet the requirements of the content security
directives. When policy directives aren't met for a resource, the browser doesn't load the
resource. For example, consider a policy that doesn't allow third-party scripts. When a
page contains a <script> tag with a third-party origin in the src attribute, the browser
prevents the script from loading.

CSP is supported in most modern desktop and mobile browsers, including Chrome,
Edge, Firefox, Opera, and Safari. CSP is recommended for Blazor apps.

Policy directives
Minimally, specify the following directives and sources for Blazor apps. Add additional
directives and sources as needed. The following directives are used in the Apply the
policy section of this article, where example security policies for Blazor WebAssembly
and Blazor Server are provided:

base-uri : Restricts the URLs for a page's <base> tag. Specify self to indicate that
the app's origin, including the scheme and port number, is a valid source.
default-src : Indicates a fallback for source directives that aren't explicitly
specified by the policy. Specify self to indicate that the app's origin, including the
scheme and port number, is a valid source.
img-src : Indicates valid sources for images.
Specify data: to permit loading images from data: URLs.
Specify https: to permit loading images from HTTPS endpoints.
object-src : Indicates valid sources for the <object> , <embed> , and <applet> tags.
Specify none to prevent all URL sources.
script-src : Indicates valid sources for scripts.
Specify self to indicate that the app's origin, including the scheme and port
number, is a valid source.
In a Blazor WebAssembly app:
Specify unsafe-eval to permit the Blazor WebAssembly Mono runtime to
function.
Specify any additional hashes to permit your required non-framework scripts
to load.
In a Blazor Server app, specify hashes to permit required scripts to load.
style-src : Indicates valid sources for stylesheets.
Specify self to indicate that the app's origin, including the scheme and port
number, is a valid source.
If the app uses inline styles, specify unsafe-inline to allow the use of your
inline styles.
upgrade-insecure-requests : Indicates that content URLs from insecure (HTTP)
sources should be acquired securely over HTTPS.

The preceding directives are supported by all browsers except Microsoft Internet
Explorer.

To obtain SHA hashes for additional inline scripts:

Apply the CSP shown in the Apply the policy section.


Access the browser's developer tools console while running the app locally. The
browser calculates and displays hashes for blocked scripts when a CSP header or
meta tag is present.

Copy the hashes provided by the browser to the script-src sources. Use single
quotes around each hash.

For a Content Security Policy Level 2 browser support matrix, see Can I use: Content
Security Policy Level 2 .

Apply the policy


Use a <meta> tag to apply the policy:
Set the value of the http-equiv attribute to Content-Security-Policy .
Place the directives in the content attribute value. Separate directives with a
semicolon ( ; ).
Always place the meta tag in the <head> content.

The following sections show example policies for Blazor WebAssembly and Blazor
Server. These examples are versioned with this article for each release of Blazor. To use a
version appropriate for your release, select the document version with the Version
dropdown selector on this webpage.

Blazor WebAssembly
In the <head> content of the wwwroot/index.html host page, apply the directives
described in the Policy directives section:

HTML

<meta http-equiv="Content-Security-Policy"
content="base-uri 'self';
default-src 'self';
img-src data: https:;
object-src 'none';
script-src 'self'
'unsafe-eval';
style-src 'self';
upgrade-insecure-requests;">

Add additional script-src and style-src hashes as required by the app. During
development, use an online tool or browser developer tools to have the hashes
calculated for you. For example, the following browser tools console error reports the
hash for a required script not covered by the policy:

Refused to execute inline script because it violates the following Content Security
Policy directive: " ... ". Either the 'unsafe-inline' keyword, a hash ('sha256-
v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA='), or a nonce
('nonce-...') is required to enable inline execution.

The particular script associated with the error is displayed in the console next to the
error.

Blazor Server
In the <head> markup (location of <head> content), apply the directives described in
the Policy directives section:

HTML

<meta http-equiv="Content-Security-Policy"
content="base-uri 'self';
default-src 'self';
img-src data: https:;
object-src 'none';
script-src 'self';
style-src 'self';
upgrade-insecure-requests;">

Add additional script-src and style-src hashes as required by the app. During
development, use an online tool or browser developer tools to have the hashes
calculated for you. For example, the following browser tools console error reports the
hash for a required script not covered by the policy:

Refused to execute inline script because it violates the following Content Security
Policy directive: " ... ". Either the 'unsafe-inline' keyword, a hash ('sha256-
v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA='), or a nonce
('nonce-...') is required to enable inline execution.

The particular script associated with the error is displayed in the console next to the
error.

Meta tag limitations


A <meta> tag policy doesn't support the following directives:

frame-ancestors
report-to
report-uri
sandbox

To support the preceding directives, use a header named Content-Security-Policy . The


directive string is the header's value.

Test a policy and receive violation reports


Testing helps confirm that third-party scripts aren't inadvertently blocked when building
an initial policy.

To test a policy over a period of time without enforcing the policy directives, set the
<meta> tag's http-equiv attribute or header name of a header-based policy to Content-
Security-Policy-Report-Only . Failure reports are sent as JSON documents to a specified

URL. For more information, see MDN web docs: Content-Security-Policy-Report-Only .

For reporting on violations while a policy is active, see the following articles:

report-to
report-uri

Although report-uri is no longer recommended for use, both directives should be used
until report-to is supported by all of the major browsers. Don't exclusively use report-
uri because support for report-uri is subject to being dropped at any time from
browsers. Remove support for report-uri in your policies when report-to is fully
supported. To track adoption of report-to , see Can I use: report-to .

Test and update an app's policy every release.

Troubleshoot
Errors appear in the browser's developer tools console. Browsers provide
information about:
Elements that don't comply with the policy.
How to modify the policy to allow for a blocked item.
A policy is only completely effective when the client's browser supports all of the
included directives. For a current browser support matrix, see Can I use: Content-
Security-Policy .

Additional resources
Apply a CSP in C# code at startup
MDN web docs: Content-Security-Policy
Content Security Policy Level 2
Google CSP Evaluator
ASP.NET Core Blazor state management
Article • 05/23/2023

This article describes common approaches for maintaining a user's data (state) while
they use an app and across browser sessions.

Blazor Server is a stateful app framework. Most of the time, the app maintains a
connection to the server. The user's state is held in the server's memory in a circuit.

Examples of user state held in a circuit include:

The hierarchy of component instances and their most recent render output in the
rendered UI.
The values of fields and properties in component instances.
Data held in dependency injection (DI) service instances that are scoped to the
circuit.

User state might also be found in JavaScript variables in the browser's memory set via
JavaScript interop calls.

If a user experiences a temporary network connection loss, Blazor attempts to reconnect


the user to their original circuit with their original state. However, reconnecting a user to
their original circuit in the server's memory isn't always possible:

The server can't retain a disconnected circuit forever. The server must release a
disconnected circuit after a timeout or when the server is under memory pressure.
In multi-server, load-balanced deployment environments, individual servers may
fail or be automatically removed when no longer required to handle the overall
volume of requests. The original server processing requests for a user may become
unavailable when the user attempts to reconnect.
The user might close and re-open their browser or reload the page, which removes
any state held in the browser's memory. For example, JavaScript variable values set
through JavaScript interop calls are lost.

When a user can't be reconnected to their original circuit, the user receives a new circuit
with an empty state. This is equivalent to closing and re-opening a desktop app.

Persist state across circuits


Generally, maintain state across circuits where users are actively creating data, not
simply reading data that already exists.
To preserve state across circuits, the app must persist the data to some other storage
location than the server's memory. State persistence isn't automatic. You must take steps
when developing the app to implement stateful data persistence.

Data persistence is typically only required for high-value state that users expended
effort to create. In the following examples, persisting state either saves time or aids in
commercial activities:

Multi-step web forms: It's time-consuming for a user to re-enter data for several
completed steps of a multi-step web form if their state is lost. A user loses state in
this scenario if they navigate away from the form and return later.
Shopping carts: Any commercially important component of an app that represents
potential revenue can be maintained. A user who loses their state, and thus their
shopping cart, may purchase fewer products or services when they return to the
site later.

An app can only persist app state. UIs can't be persisted, such as component instances
and their render trees. Components and render trees aren't generally serializable. To
persist UI state, such as the expanded nodes of a tree view control, the app must use
custom code to model the behavior of the UI state as serializable app state.

Where to persist state


Common locations exist for persisting state:

Server-side storage
URL
Browser storage
In-memory state container service

Server-side storage
For permanent data persistence that spans multiple users and devices, the app can use
server-side storage. Options include:

Blob storage
Key-value storage
Relational database
Table storage

After data is saved, the user's state is retained and available in any new circuit.
For more information on Azure data storage options, see the following:

Azure Databases
Azure Storage Documentation

URL
For transient data representing navigation state, model the data as a part of the URL.
Examples of user state modeled in the URL include:

The ID of a viewed entity.


The current page number in a paged grid.

The contents of the browser's address bar are retained:

If the user manually reloads the page.


If the web server becomes unavailable, and the user is forced to reload the page in
order to connect to a different server.

For information on defining URL patterns with the @page directive, see ASP.NET Core
Blazor routing and navigation.

Browser storage
For transient data that the user is actively creating, a commonly used storage location is
the browser's localStorage and sessionStorage collections:

localStorage is scoped to the browser's window. If the user reloads the page or
closes and re-opens the browser, the state persists. If the user opens multiple
browser tabs, the state is shared across the tabs. Data persists in localStorage
until explicitly cleared.
sessionStorage is scoped to the browser tab. If the user reloads the tab, the state

persists. If the user closes the tab or the browser, the state is lost. If the user opens
multiple browser tabs, each tab has its own independent version of the data.

Generally, sessionStorage is safer to use. sessionStorage avoids the risk that a user
opens multiple tabs and encounters the following:

Bugs in state storage across tabs.


Confusing behavior when a tab overwrites the state of other tabs.

localStorage is the better choice if the app must persist state across closing and re-

opening the browser.


Caveats for using browser storage:

Similar to the use of a server-side database, loading and saving data are
asynchronous.
Unlike a server-side database, storage isn't available during prerendering because
the requested page doesn't exist in the browser during the prerendering stage.
Storage of a few kilobytes of data is reasonable to persist for Blazor Server apps.
Beyond a few kilobytes, you must consider the performance implications because
the data is loaded and saved across the network.
Users may view or tamper with the data. ASP.NET Core Data Protection can
mitigate the risk. For example, ASP.NET Core Protected Browser Storage uses
ASP.NET Core Data Protection.

Third-party NuGet packages provide APIs for working with localStorage and
sessionStorage . It's worth considering choosing a package that transparently uses

ASP.NET Core Data Protection. Data Protection encrypts stored data and reduces the
potential risk of tampering with stored data. If JSON-serialized data is stored in plain
text, users can see the data using browser developer tools and also modify the stored
data. Securing data isn't always a problem because the data might be trivial in nature.
For example, reading or modifying the stored color of a UI element isn't a significant
security risk to the user or the organization. Avoid allowing users to inspect or tamper
with sensitive data.

ASP.NET Core Protected Browser Storage


ASP.NET Core Protected Browser Storage leverages ASP.NET Core Data Protection for
localStorage and sessionStorage .

7 Note

Protected Browser Storage relies on ASP.NET Core Data Protection and is only
supported for Blazor Server apps.

Save and load data within a component


In any component that requires loading or saving data to browser storage, use the
@inject directive to inject an instance of either of the following:

ProtectedLocalStorage

ProtectedSessionStorage
The choice depends on which browser storage location you wish to use. In the following
example, sessionStorage is used:

razor

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

The @using directive can be placed in the app's _Imports.razor file instead of in the
component. Use of the _Imports.razor file makes the namespace available to larger
segments of the app or the whole app.

To persist the currentCount value in the Counter component of an app based on the
Blazor Server project template, modify the IncrementCount method to use
ProtectedSessionStore.SetAsync :

C#

private async Task IncrementCount()


{
currentCount++;
await ProtectedSessionStore.SetAsync("count", currentCount);
}

In larger, more realistic apps, storage of individual fields is an unlikely scenario. Apps are
more likely to store entire model objects that include complex state.
ProtectedSessionStore automatically serializes and deserializes JSON data to store
complex state objects.

In the preceding code example, the currentCount data is stored as


sessionStorage['count'] in the user's browser. The data isn't stored in plain text but
rather is protected using ASP.NET Core Data Protection. The encrypted data can be
inspected if sessionStorage['count'] is evaluated in the browser's developer console.

To recover the currentCount data if the user returns to the Counter component later,
including if the user is on a new circuit, use ProtectedSessionStore.GetAsync :

C#

protected override async Task OnInitializedAsync()


{
var result = await ProtectedSessionStore.GetAsync<int>("count");
currentCount = result.Success ? result.Value : 0;
}
If the component's parameters include navigation state, call
ProtectedSessionStore.GetAsync and assign a non- null result in
OnParametersSetAsync, not OnInitializedAsync. OnInitializedAsync is only called once
when the component is first instantiated. OnInitializedAsync isn't called again later if the
user navigates to a different URL while remaining on the same page. For more
information, see ASP.NET Core Razor component lifecycle.

2 Warning

The examples in this section only work if the server doesn't have prerendering
enabled. With prerendering enabled, an error is generated explaining that
JavaScript interop calls cannot be issued because the component is being
prerendered.

Either disable prerendering or add additional code to work with prerendering. To


learn more about writing code that works with prerendering, see the Handle
prerendering section.

Handle the loading state


Since browser storage is accessed asynchronously over a network connection, there's
always a period of time before the data is loaded and available to a component. For the
best results, render a loading-state message while loading is in progress instead of
displaying blank or default data.

One approach is to track whether the data is null , which means that the data is still
loading. In the default Counter component, the count is held in an int . Make
currentCount nullable by adding a question mark ( ? ) to the type ( int ):

C#

private int? currentCount;

Instead of unconditionally displaying the count and Increment button, display these
elements only if the data is loaded by checking HasValue:

razor

@if (currentCount.HasValue)
{
<p>Current count: <strong>@currentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>
}
else
{
<p>Loading...</p>
}

Handle prerendering
During prerendering:

An interactive connection to the user's browser doesn't exist.


The browser doesn't yet have a page in which it can run JavaScript code.

localStorage or sessionStorage aren't available during prerendering. If the component


attempts to interact with storage, an error is generated explaining that JavaScript
interop calls cannot be issued because the component is being prerendered.

One way to resolve the error is to disable prerendering. This is usually the best choice if
the app makes heavy use of browser-based storage. Prerendering adds complexity and
doesn't benefit the app because the app can't prerender any useful content until
localStorage or sessionStorage are available.

To disable prerendering, open the Pages/_Host.cshtml file and change the render-mode
attribute of the Component Tag Helper to Server:

CSHTML

<component type="typeof(App)" render-mode="Server" />

Prerendering of <head> content is disabled in <head> content:

CSHTML

<component type="typeof(HeadOutlet)" render-mode="Server" />

Prerendering might be useful for other pages that don't use localStorage or
sessionStorage . To retain prerendering, defer the loading operation until the browser is

connected to the circuit. The following is an example for storing a counter value:

razor

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore
@if (isConnected)
{
<p>Current count: <strong>@currentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>
}
else
{
<p>Loading...</p>
}

@code {
private int currentCount;
private bool isConnected;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
isConnected = true;
await LoadStateAsync();
StateHasChanged();
}
}

private async Task LoadStateAsync()


{
var result = await ProtectedLocalStore.GetAsync<int>("count");
currentCount = result.Success ? result.Value : 0;
}

private async Task IncrementCount()


{
currentCount++;
await ProtectedLocalStore.SetAsync("count", currentCount);
}
}

Factor out the state preservation to a common location


If many components rely on browser-based storage, re-implementing state provider
code many times creates code duplication. One option for avoiding code duplication is
to create a state provider parent component that encapsulates the state provider logic.
Child components can work with persisted data without regard to the state persistence
mechanism.

In the following example of a CounterStateProvider component, counter data is


persisted to sessionStorage :

razor
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
<CascadingValue Value="@this">
@ChildContent
</CascadingValue>
}
else
{
<p>Loading...</p>
}

@code {
private bool isLoaded;

[Parameter]
public RenderFragment? ChildContent { get; set; }

public int CurrentCount { get; set; }

protected override async Task OnInitializedAsync()


{
var result = await ProtectedSessionStore.GetAsync<int>("count");
CurrentCount = result.Success ? result.Value : 0;
isLoaded = true;
}

public async Task SaveChangesAsync()


{
await ProtectedSessionStore.SetAsync("count", CurrentCount);
}
}

7 Note

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

The CounterStateProvider component handles the loading phase by not rendering its
child content until state loading is complete.

To use the CounterStateProvider component, wrap an instance of the component


around any other component that requires access to the counter state. To make the
state accessible to all components in an app, wrap the CounterStateProvider
component around the Router in the App component ( App.razor ):

razor
<CounterStateProvider>
<Router AppAssembly="@typeof(App).Assembly">
...
</Router>
</CounterStateProvider>

Wrapped components receive and can modify the persisted counter state. The following
Counter component implements the pattern:

razor

@page "/counter"

<p>Current count: <strong>@CounterStateProvider?.CurrentCount</strong></p>


<button @onclick="IncrementCount">Increment</button>

@code {
[CascadingParameter]
private CounterStateProvider? CounterStateProvider { get; set; }

private async Task IncrementCount()


{
if (CounterStateProvider is not null)
{
CounterStateProvider.CurrentCount++;
await CounterStateProvider.SaveChangesAsync();
}
}
}

The preceding component isn't required to interact with ProtectedBrowserStorage , nor


does it deal with a "loading" phase.

To deal with prerendering as described earlier, CounterStateProvider can be amended


so that all of the components that consume the counter data automatically work with
prerendering. For more information, see the Handle prerendering section.

In general, the state provider parent component pattern is recommended:

To consume state across many components.


If there's just one top-level state object to persist.

To persist many different state objects and consume different subsets of objects in
different places, it's better to avoid persisting state globally.

In-memory state container service


Nested components typically bind data using chained bind as described in ASP.NET Core
Blazor data binding. Nested and unnested components can share access to data using a
registered in-memory state container. A custom state container class can use an
assignable Action to notify components in different parts of the app of state changes. In
the following example:

A pair of components uses a state container to track a property.


One component in the following example is nested in the other component, but
nesting isn't required for this approach to work.

) Important

The example in this section demonstrates how to create an in-memory state


container service, register the service, and use the service in components. The
example doesn't persist data without further development. For persistent storage
of data, the state container must adopt an underlying storage mechanism that
survives when browser memory is cleared. This can be accomplished with
localStorage / sessionStorage or some other technology.

StateContainer.cs :

C#

public class StateContainer


{
private string? savedString;

public string Property


{
get => savedString ?? string.Empty;
set
{
savedString = value;
NotifyStateChanged();
}
}

public event Action? OnChange;

private void NotifyStateChanged() => OnChange?.Invoke();


}

In Program.cs (Blazor WebAssembly):

C#
builder.Services.AddSingleton<StateContainer>();

In Program.cs (Blazor Server) in ASP.NET Core 6.0 or later:

C#

builder.Services.AddScoped<StateContainer>();

In Startup.ConfigureServices (Blazor Server) in versions of ASP.NET Core earlier than


6.0:

C#

services.AddScoped<StateContainer>();

Shared/Nested.razor :

razor

@implements IDisposable
@inject StateContainer StateContainer

<h2>Nested component</h2>

<p>Nested component Property: <b>@StateContainer.Property</b></p>

<p>
<button @onclick="ChangePropertyValue">
Change the Property from the Nested component
</button>
</p>

@code {
protected override void OnInitialized()
{
StateContainer.OnChange += StateHasChanged;
}

private void ChangePropertyValue()


{
StateContainer.Property =
$"New value set in the Nested component: {DateTime.Now}";
}

public void Dispose()


{
StateContainer.OnChange -= StateHasChanged;
}
}

Pages/StateContainerExample.razor :

razor

@page "/state-container-example"
@implements IDisposable
@inject StateContainer StateContainer

<h1>State Container Example component</h1>

<p>State Container component Property: <b>@StateContainer.Property</b></p>

<p>
<button @onclick="ChangePropertyValue">
Change the Property from the State Container Example component
</button>
</p>

<Nested />

@code {
protected override void OnInitialized()
{
StateContainer.OnChange += StateHasChanged;
}

private void ChangePropertyValue()


{
StateContainer.Property = "New value set in the State " +
$"Container Example component: {DateTime.Now}";
}

public void Dispose()


{
StateContainer.OnChange -= StateHasChanged;
}
}

The preceding components implement IDisposable, and the OnChange delegates are
unsubscribed in the Dispose methods, which are called by the framework when the
components are disposed. For more information, see ASP.NET Core Razor component
lifecycle.
Debug ASP.NET Core Blazor
WebAssembly
Article • 05/23/2023

This article describes how to debug Blazor WebAssembly with browser developer tools
and an integrated development environment (IDE).

Blazor WebAssembly apps can be debugged using the browser developer tools in
Chromium-based browsers (Edge/Chrome). You can also debug your app using the
following IDEs:

Visual Studio
Visual Studio for Mac
Visual Studio Code

Available scenarios include:

Set and remove breakpoints.


Run the app with debugging support in IDEs.
Single-step through the code.
Resume code execution with a keyboard shortcut in IDEs.
In the Locals window, observe the values of local variables.
See the call stack, including call chains between JavaScript and .NET.

For now, you can't:

Break on unhandled exceptions.


Hit breakpoints during app startup before the debug proxy is running. This
includes breakpoints in Program.cs and breakpoints in the OnInitialized{Async}
lifecycle methods of components that are loaded by the first page requested from
the app.
Debug in non-local scenarios (for example, Windows Subsystem for Linux (WSL) or
Visual Studio Codespaces ).
Automatically rebuild the backend Server app of a hosted Blazor WebAssembly
solution during debugging, for example by running the app with dotnet watch run.
Use a symbol server for debugging.

Prerequisites
Debugging requires the latest version of the following browsers:
Google Chrome (default)
Microsoft Edge

Ensure that firewalls or proxies don't block communication with the debug proxy
( NodeJS process). For more information, see the Firewall configuration section.

Visual Studio Code users require the C# for Visual Studio Code Extension .

Visual Studio for Mac requires version 8.8 (build 1532) or later. Install the latest release
of Visual Studio for Mac by selecting the Download Visual Studio for Mac button at
Microsoft: Visual Studio for Mac .

7 Note

Apple Safari on macOS isn't currently supported.

Packages
Standalone Blazor WebAssembly:
Microsoft.AspNetCore.Components.WebAssembly.DevServer : Development server for
use when building Blazor apps. Calls
WebAssemblyNetDebugProxyAppBuilderExtensions.UseWebAssemblyDebugging
internally to add middleware for debugging Blazor WebAssembly apps inside Chromium
developer tools.

Hosted Blazor WebAssembly:

Client project: Microsoft.AspNetCore.Components.WebAssembly.DevServer :


Development server for use when building Blazor apps. Calls
WebAssemblyNetDebugProxyAppBuilderExtensions.UseWebAssemblyDebugging
internally to add middleware for debugging Blazor WebAssembly apps inside
Chromium developer tools.
Server project: Microsoft.AspNetCore.Components.WebAssembly.Server :
References an internal package
(Microsoft.NETCore.BrowserDebugHost.Transport ) for assemblies that share the
browser debug host.

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 .

Debug a standalone Blazor WebAssembly app


To enable debugging for an existing Blazor WebAssembly app, update the
launchSettings.json file in the startup project to include the following inspectUri
property in each launch profile:

JSON

"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-
proxy?browser={browserInspectUri}"

Once updated, the launchSettings.json file should look similar to the following
example:

JSON

{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50454",
"sslPort": 44399
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:
{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"BlazorApp1.Server": {
"commandName": "Project",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:
{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

The inspectUri property:

Enables the IDE to detect that the app is a Blazor WebAssembly app.
Instructs the script debugging infrastructure to connect to the browser through
Blazor's debugging proxy.

The placeholder values for the WebSocket protocol ( wsProtocol ), host ( url.hostname ),
port ( url.port ), and inspector URI on the launched browser ( browserInspectUri ) are
provided by the framework.

Visual Studio

To debug a Blazor WebAssembly app in Visual Studio:

1. Create a new hosted Blazor WebAssembly solution.

2. With the Server project selected in Solution Explorer, press F5 to run the app
in the debugger.

7 Note

When debugging with a Chromium-based browser, such as Google


Chrome or Microsoft Edge, a new browser window might open with a
separate profile for the debugging session instead of opening a tab in an
existing browser window with the user's profile. If debugging with the
user's profile is a requirement, adopt one of the following approaches:

Close all open browser instances before pressing F5 to start


debugging.
Configure Visual Studio to launch the browser with the user's
profile. For more information on this approach, see Blazor WASM
Debugging in VS launches Edge with a separate user data
directory (dotnet/aspnetcore #20915) .

7 Note

Start Without Debugging [ Ctrl + F5 (Windows) or ⌘ + F5 (macOS)]


isn't supported. When the app is run in Debug configuration, debugging
overhead always results in a small performance reduction.

3. In the Client app, set a breakpoint on the currentCount++; line in


Pages/Counter.razor .

4. In the browser, navigate to Counter page and select the Click me button to hit
the breakpoint.

5. In Visual Studio, inspect the value of the currentCount field in the Locals
window.

6. Press F5 to continue execution.

While debugging a Blazor WebAssembly app, you can also debug server code:

1. Set a breakpoint in the Pages/FetchData.razor page in OnInitializedAsync.


2. Set a breakpoint in the WeatherForecastController in the Get action method.
3. Browse to the Fetch Data page to hit the first breakpoint in the FetchData
component just before it issues an HTTP request to the server.
4. Press F5 to continue execution and then hit the breakpoint on the server in
the WeatherForecastController .
5. Press F5 again to let execution continue and see the weather forecast table
rendered in the browser.

7 Note

Breakpoints are not hit during app startup before the debug proxy is running.
This includes breakpoints in Program.cs and breakpoints in the
OnInitialized{Async} lifecycle methods of components that are loaded by the
first page requested from the app.

Debug standalone Blazor WebAssembly


For information on configuring VS Code assets in the .vscode folder, see the Linux
operating system guidance in Tooling for ASP.NET Core Blazor.

1. Open the standalone Blazor WebAssembly app in VS Code.

You may receive a notification that additional setup is required to enable


debugging:
Additional setup is required to debug Blazor WebAssembly applications.

If you receive the notification:

Confirm that the latest C# for Visual Studio Code Extension is installed.
To inspect the installed extensions, open View > Extensions from the
menu bar or select the Extensions icon in the Activity sidebar.
When using the C# for Visual Studio Code Extension version 1.23.9 or
later, confirm that the latest Blazor WASM Debugging Extension is
installed. To inspect the installed extensions, open View > Extensions
from the menu bar or select the Extensions icon in the Activity sidebar.
Reload the window.

2. Create a .vscode/launch.json file with the following configuration. Replace


the {PORT} placeholder with the port configured in
Properties/launchSettings.json :

JSON

{
"name": "Launch and Debug",
"type": "blazorwasm",
"request": "launch",
"url": "http://localhost:{PORT}"
}

If the app is in a subfolder of the workspace root, include the current working
directory ( cwd ) property with the path to the app. In the following property
value, replace the {PATH} placeholder with the path to the app:

JSON

"cwd": "${workspaceFolder}/{PATH}"

In the following example, the app is in a subfolder named blazorwasm :

JSON

"cwd": "${workspaceFolder}/blazorwasm"

3. Start debugging using the F5 keyboard shortcut or the menu command.

7 Note
Start Without Debugging [ Ctrl + F5 (Windows) or ⌘ + F5 (macOS)]
isn't supported. When the app is run in Debug configuration, debugging
overhead always results in a small performance reduction.

4. The standalone app is launched, and a debugging browser is opened.

5. In the Client app, set a breakpoint on the currentCount++; line in


Pages/Counter.razor .

6. In the browser, navigate to Counter page and select the Click me button to hit
the breakpoint.

7 Note

Breakpoints are not hit during app startup before the debug proxy is running.
This includes breakpoints in Program.cs and breakpoints in the
OnInitialized{Async} lifecycle methods of components that are loaded by the
first page requested from the app.

Debug hosted Blazor WebAssembly


For guidance on configuring VS Code assets in the .vscode folder and where to
place the .vscode folder in the solution, see the Linux operating system guidance
in Tooling for ASP.NET Core Blazor.

7 Note

Only browser debugging is supported at this time.

You can't automatically rebuild the backend Server app of a hosted Blazor
WebAssembly solution during debugging, for example by running the app
with dotnet watch run.

To debug a published, hosted Blazor WebAssembly app, configure debugger


support ( DebuggerSupport ) and copy output symbols to the publish directory
( CopyOutputSymbolsToPublishDirectory ) in the Client app's project file:

XML

<DebuggerSupport>true</DebuggerSupport>
<CopyOutputSymbolsToPublishDirectory>true</CopyOutputSymbolsToPublishDir
ectory>

By default, publishing an app disables the preceding properties by setting them to


false .

2 Warning

Published, hosted Blazor WebAssembly apps should only enable debugging


and copying output symbols when deploying published assets locally. Do not*
deploy a published app into production with the DebuggerSupport and
CopyOutputSymbolsToPublishDirectory properties set to true .

Attach to an existing debugging session


To attach to a running Blazor app, create a .vscode/launch.json file with the
following configuration. Replace the {URL} placeholder with the URL where the app
is running:

JSON

{
"name": "Attach and Debug"
"type": "blazorwasm",
"request": "attach",
"url": "{URL}"
}

7 Note

Attaching to a debugging session is only supported for standalone apps. To


use full-stack debugging, you must launch the app from VS Code.

Launch configuration options


The following launch configuration options are supported for the blazorwasm
debug type ( .vscode/launch.json ).

Option Description
Option Description

request Use launch to launch and attach a debugging session to a Blazor WebAssembly
app or attach to attach a debugging session to an already-running app.

url The URL to open in the browser when debugging.

browser The browser to launch for the debugging session. Set to edge or chrome . Defaults
to edge .

trace Used to generate logs from the JS debugger. Set to true to generate logs.

hosted Must be set to true if launching and debugging a hosted Blazor WebAssembly
app.

webRoot Specifies the absolute path of the web server. Should be set if an app is served
from a sub-route.

timeout The number of milliseconds to wait for the debugging session to attach. Defaults
to 30,000 milliseconds (30 seconds).

program A reference to the executable to run the server of the hosted app. Must be set if
hosted is true .

cwd The working directory to launch the app under. Must be set if hosted is true .

env The environment variables to provide to the launched process. Only applicable if
hosted is set to true .

Debug in the browser


The guidance in this section applies to Google Chrome and Microsoft Edge running on
Windows.

1. Run a Debug build of the app in the Development environment.

2. Launch a browser and navigate to the app's URL.

3. In the browser, attempt to commence remote debugging by pressing Shift + Alt

+ d .

The browser must be running with remote debugging enabled, which isn't the
default. If remote debugging is disabled, an Unable to find debuggable browser
tab error page is rendered with instructions for launching the browser with the
debugging port open. Follow the instructions for your browser, which opens a new
browser window. Close the previous browser window.
1. Once the browser is running with remote debugging enabled, the debugging
keyboard shortcut in the previous step opens a new debugger tab.

2. After a moment, the Sources tab shows a list of the app's .NET assemblies within
the file:// node.

3. In component code ( .razor files) and C# code files ( .cs ), breakpoints that you set
are hit when code executes. After a breakpoint is hit, single-step ( F10 ) through the
code or resume ( F8 ) code execution normally.

Blazor provides a debugging proxy that implements the Chrome DevTools Protocol
and augments the protocol with .NET-specific information. When debugging keyboard
shortcut is pressed, Blazor points the Chrome DevTools at the proxy. The proxy connects
to the browser window you're seeking to debug (hence the need to enable remote
debugging).

Browser source maps


Browser source maps allow the browser to map compiled files back to their original
source files and are commonly used for client-side debugging. However, Blazor doesn't
currently map C# directly to JavaScript/WASM. Instead, Blazor does IL interpretation
within the browser, so source maps aren't relevant.

Firewall configuration
If a firewall blocks communication with the debug proxy, create a firewall exception rule
that permits communication between the browser and the NodeJS process.

2 Warning

Modification of a firewall configuration must be made with care to avoid creating


security vulnerabilities. Carefully apply security guidance, follow best security
practices, and respect warnings issued by the firewall's manufacturer.

Permitting open communication with the NodeJS process:

Opens up the Node server to any connection, depending on the firewall's


capabilities and configuration.
Might be risky depending on your network.
Is only recommended on developer machines.
If possible, only allow open communication with the NodeJS process on trusted or
private networks.

For Windows Firewall configuration guidance, see Create an Inbound Program or Service
Rule. For more information, see Windows Defender Firewall with Advanced Security and
related articles in the Windows Firewall documentation set.

Troubleshoot
If you're running into errors, the following tips may help:

In the Debugger tab, open the developer tools in your browser. In the console,
execute localStorage.clear() to remove any breakpoints.
Confirm that you've installed and trusted the ASP.NET Core HTTPS development
certificate. For more information, see Enforce HTTPS in ASP.NET Core.
Visual Studio requires the Enable JavaScript debugging for ASP.NET (Chrome,
Edge and IE) option in Tools > Options > Debugging > General. This is the
default setting for Visual Studio. If debugging isn't working, confirm that the
option is selected.
If your environment uses an HTTP proxy, make sure that localhost is included in
the proxy bypass settings. This can be done by setting the NO_PROXY environment
variable in either:
The launchSettings.json file for the project.
At the user or system environment variables level for it to apply to all apps.
When using an environment variable, restart Visual Studio for the change to
take effect.
Ensure that firewalls or proxies don't block communication with the debug proxy
( NodeJS process). For more information, see the Firewall configuration section.

Breakpoints in OnInitialized{Async} not hit


The Blazor framework's debugging proxy takes a short time to launch, so breakpoints in
the OnInitialized{Async} lifecycle methods might not be hit. We recommend adding a
delay at the start of the method body to give the debug proxy some time to launch
before the breakpoint is hit. You can include the delay based on an if compiler directive
to ensure that the delay isn't present for a release build of the app.

OnInitialized:

C#
protected override void OnInitialized()
{
#if DEBUG
Thread.Sleep(10000);
#endif

...
}

OnInitializedAsync:

C#

protected override async Task OnInitializedAsync()


{
#if DEBUG
await Task.Delay(10000);
#endif

...
}

Visual Studio (Windows) timeout


If Visual Studio throws an exception that the debug adapter failed to launch mentioning
that the timeout was reached, you can adjust the timeout with a Registry setting:

Console

VsRegEdit.exe set "<VSInstallFolder>" HKCU JSDebugger\Options\Debugging


"BlazorTimeoutInMilliseconds" dword {TIMEOUT}

The {TIMEOUT} placeholder in the preceding command is in milliseconds. For example,


one minute is assigned as 60000 .
Lazy load assemblies in ASP.NET Core
Blazor WebAssembly
Article • 03/08/2023

Blazor WebAssembly app startup performance can be improved by waiting to load app
assemblies until the assemblies are required, which is called lazy loading.

This article's initial sections cover the app configuration. For a working demonstration,
see the Complete example section at the end of this article.

This article only applies to Blazor WebAssembly apps. Assembly lazy loading doesn't
benefit Blazor Server apps because Blazor Server app assemblies aren't downloaded to
the client.

Project file configuration


Mark assemblies for lazy loading in the app's project file ( .csproj ) using the
BlazorWebAssemblyLazyLoad item. Use the assembly name with the .dll extension. The

Blazor framework prevents the assembly from loading at app launch.

XML

<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="{ASSEMBLY NAME}.dll" />
</ItemGroup>

The {ASSEMBLY NAME} placeholder is the name of the assembly. The .dll file extension is
required.

Include one BlazorWebAssemblyLazyLoad item for each assembly. If an assembly has


dependencies, include a BlazorWebAssemblyLazyLoad entry for each dependency.

Router component configuration


The Blazor framework automatically registers a singleton service for lazy loading
assemblies in client-side Blazor WebAssembly apps†, LazyAssemblyLoader. The
LazyAssemblyLoader.LoadAssembliesAsync method:

Uses JS interop to fetch assemblies via a network call.


Loads assemblies into the runtime executing on WebAssembly in the browser.
†Guidance for hosted Blazor WebAssembly solutions is covered in the Lazy load
assemblies in a hosted Blazor WebAssembly solution section.

Blazor's Router component designates the assemblies that Blazor searches for routable
components and is also responsible for rendering the component for the route where
the user navigates. The Router component's OnNavigateAsync method is used in
conjunction with lazy loading to load the correct assemblies for endpoints that a user
requests.

Logic is implemented inside OnNavigateAsync to determine the assemblies to load with


LazyAssemblyLoader. Options for how to structure the logic include:

Conditional checks inside the OnNavigateAsync method.


A lookup table that maps routes to assembly names, either injected into the
component or implemented within the @code block.

In the following example:

The namespace for Microsoft.AspNetCore.Components.WebAssembly.Services is


specified.
The LazyAssemblyLoader service is injected ( AssemblyLoader ).
The {PATH} placeholder is the path where the list of assemblies should load. The
example uses a conditional check for a single path that loads a single set of
assemblies.
The {LIST OF ASSEMBLIES} placeholder is the comma-separated list of assembly file
name strings, including their .dll extensions (for example, "Assembly1.dll",
"Assembly2.dll" ).

App.razor :

razor

@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="@typeof(App).Assembly"
OnNavigateAsync="@OnNavigateAsync">
...
</Router>

@code {
private async Task OnNavigateAsync(NavigationContext args)
{
try
{
if (args.Path == "{PATH}")
{
var assemblies = await
AssemblyLoader.LoadAssembliesAsync(
new[] { {LIST OF ASSEMBLIES} });
}
}
catch (Exception ex)
{
Logger.LogError("Error: {Message}", ex.Message);
}
}
}

7 Note

The preceding example doesn't show the contents of the Router component's
Razor markup ( ... ). For a demonstration with complete code, see the Complete
example section of this article.

Assemblies that include routable components


When the list of assemblies includes routable components, the assembly list for a given
path is passed to the Router component's AdditionalAssemblies collection.

In the following example:

The List<Assembly> in lazyLoadedAssemblies passes the assembly list to


AdditionalAssemblies. The framework searches the assemblies for routes and
updates the route collection if new routes are found. To access the Assembly type,
the namespace for System.Reflection is included at the top of the App.razor file.
The {PATH} placeholder is the path where the list of assemblies should load. The
example uses a conditional check for a single path that loads a single set of
assemblies.
The {LIST OF ASSEMBLIES} placeholder is the comma-separated list of assembly file
name strings, including their .dll extensions (for example, "Assembly1.dll",
"Assembly2.dll" ).

App.razor :

razor
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="@lazyLoadedAssemblies"
OnNavigateAsync="@OnNavigateAsync">
...
</Router>

@code {
private List<Assembly> lazyLoadedAssemblies = new();

private async Task OnNavigateAsync(NavigationContext args)


{
try
{
if (args.Path == "{PATH}")
{
var assemblies = await
AssemblyLoader.LoadAssembliesAsync(
new[] { {LIST OF ASSEMBLIES} });
lazyLoadedAssemblies.AddRange(assemblies);
}
}
catch (Exception ex)
{
Logger.LogError("Error: {Message}", ex.Message);
}
}
}

7 Note

The preceding example doesn't show the contents of the Router component's
Razor markup ( ... ). For a demonstration with complete code, see the Complete
example section of this article.

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

User interaction with <Navigating> content


While loading assemblies, which can take several seconds, the Router component can
indicate to the user that a page transition is occurring with the router's Navigating
property.

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

Handle cancellations in OnNavigateAsync


The NavigationContext object passed to the OnNavigateAsync callback contains a
CancellationToken that's set when a new navigation event occurs. The OnNavigateAsync
callback must throw when the cancellation token is set to avoid continuing to run the
OnNavigateAsync callback on an outdated navigation.

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

OnNavigateAsync events and renamed assembly


files
The resource loader relies on the assembly names that are defined in the
blazor.boot.json file. If assemblies are renamed, the assembly names used in an

OnNavigateAsync callback and the assembly names in the blazor.boot.json file are out
of sync.

To rectify this:

Check to see if the app is running in the Production environment when


determining which assembly names to use.
Store the renamed assembly names in a separate file and read from that file to
determine what assembly name to use with the LazyAssemblyLoader service and
OnNavigateAsync callback.

Lazy load assemblies in a hosted Blazor


WebAssembly solution
The framework's lazy loading implementation supports lazy loading with prerendering
in a hosted Blazor WebAssembly solution. During prerendering, all assemblies, including
those marked for lazy loading, are assumed to be loaded. Manually register the
LazyAssemblyLoader service in the Server project.

At the top of the Program.cs file of the Server project, add the namespace for
Microsoft.AspNetCore.Components.WebAssembly.Services:
C#

using Microsoft.AspNetCore.Components.WebAssembly.Services;

In Program.cs of the Server project, register the service:

C#

builder.Services.AddScoped<LazyAssemblyLoader>();

Complete example
The demonstration in this section:

Creates a robot controls assembly ( GrantImaharaRobotControls.dll ) as a Razor


class library (RCL) that includes a Robot component ( Robot.razor with a route
template of /robot ).
Lazily loads the RCL's assembly to render its Robot component when the /robot
URL is requested by the user.

1. Create a new ASP.NET Core class library project:

Visual Studio: Create a solution > Create a new project > Razor Class
Library. Name the project GrantImaharaRobotControls .
Visual Studio Code/.NET CLI: Execute dotnet new razorclasslib -o
GrantImaharaRobotControls from a command prompt. The -o|--output
option creates a folder for the solution and names the project
GrantImaharaRobotControls .

2. The example component presented later in this section uses a Blazor form. In the
RCL project, add the Microsoft.AspNetCore.Components.Forms package to the
project.

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 .

3. Create a HandGesture class in the RCL with a ThumbUp method that hypothetically
makes a robot perform a thumbs-up gesture. The method accepts an argument for
the axis, Left or Right , as an enum. The method returns true on success.

HandGesture.cs :

C#

using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls;

public static class HandGesture


{
public static bool ThumbUp(Axis axis, ILogger logger)
{
logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

// Code to make robot perform gesture

return true;
}
}

public enum Axis { Left, Right }

1. Add the following component to the root of the RCL project. The component
permits the user to submit a left or right hand thumb-up gesture request.

Robot.razor :

razor

@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="@robotModel" OnValidSubmit="@HandleValidSubmit">


<InputRadioGroup @bind-Value="robotModel.AxisSelection">
@foreach (var entry in (Axis[])Enum
.GetValues(typeof(Axis)))
{
<InputRadio Value="@entry" />
<text>&nbsp;</text>@entry<br>
}
</InputRadioGroup>

<button type="submit">Submit</button>
</EditForm>
<p>
@message
</p>

@code {
private RobotModel robotModel = new() { AxisSelection = Axis.Left };
private string? message;

private void HandleValidSubmit()


{
Logger.LogInformation("HandleValidSubmit called");

var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

message = $"ThumbUp returned {result} at {DateTime.Now}.";


}

public class RobotModel


{
public Axis AxisSelection { get; set; }
}
}

Create a Blazor WebAssembly app to demonstrate lazy loading of the RCL's assembly:

1. Create the Blazor WebAssembly app in Visual Studio, Visual Studio Code, or via a
command prompt with the .NET CLI. Name the project LazyLoadTest .

2. Create a project reference for the GrantImaharaRobotControls RCL:

Visual Studio: Add the GrantImaharaRobotControls RCL project to the solution


(Add > Existing Project). Select Add > Project Reference to add a project
reference for the GrantImaharaRobotControls RCL.
Visual Studio Code/.NET CLI: Execute dotnet add reference {PATH} in a
command shell from the project's folder. The {PATH} placeholder is the path
to the RCL project.

Build and run the app. For the default page that loads the Index component
( Pages/Index.razor ), the developer tool's Network tab indicates that the RCL's assembly
GrantImaharaRobotControls.dll is loaded. The Index component makes no use of the
assembly, so loading the assembly is inefficient.
Configure the app to lazy load the GrantImaharaRobotControls.dll assembly:

1. Specify the RCL's assembly for lazy loading in the Blazor WebAssembly app's
project file ( .csproj ):

XML

<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="GrantImaharaRobotControls.dll" />
</ItemGroup>

2. The following Router component demonstrates loading the


GrantImaharaRobotControls.dll assembly when the user navigates to /robot .
Replace the app's default App component with the following App component.
During page transitions, a styled message is displayed to the user with the
<Navigating> element. For more information, see the User interaction with
<Navigating> content section.

The assembly is assigned to AdditionalAssemblies, which results in the router


searching the assembly for routable components, where it finds the Robot
component. The Robot component's route is added to the app's route collection.
For more information, see the ASP.NET Core Blazor routing and navigation article
and the Assemblies that include routable components section of this article.

App.razor :

razor

@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="@lazyLoadedAssemblies"
OnNavigateAsync="@OnNavigateAsync">
<Navigating>
<div style="padding:20px;background-color:blue;color:white">
<p>Loading the requested page&hellip;</p>
</div>
</Navigating>
<Found Context="routeData">
<RouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

@code {
private List<Assembly> lazyLoadedAssemblies = new();

private async Task OnNavigateAsync(NavigationContext args)


{
try
{
if (args.Path == "robot")
{
var assemblies = await AssemblyLoader.LoadAssembliesAsync(
new[] { "GrantImaharaRobotControls.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
}
catch (Exception ex)
{
Logger.LogError("Error: {Message}", ex.Message);
}
}
}

Build and run the app again. For the default page that loads the Index component
( Pages/Index.razor ), the developer tool's Network tab indicates that the RCL's assembly
( GrantImaharaRobotControls.dll ) does not load for the Index component:

If the Robot component from the RCL is requested at /robot , the


GrantImaharaRobotControls.dll assembly is loaded and the Robot component is

rendered:
Troubleshoot
If unexpected rendering occurs, such as rendering a component from a previous
navigation, confirm that the code throws if the cancellation token is set.
If assemblies configured for lazy loading unexpectedly load at app start, check that
the assembly is marked for lazy loading in the project file.

Additional resources
Handle asynchronous navigation events with OnNavigateAsync
ASP.NET Core Blazor performance best practices
ASP.NET Core Blazor WebAssembly
native dependencies
Article • 04/04/2023

Blazor WebAssembly apps can use native dependencies built to run on WebAssembly.
You can statically link native dependencies into the .NET WebAssembly runtime using
the .NET WebAssembly build tools, the same tools used to ahead-of-time (AOT)
compile a Blazor app to WebAssembly and to relink the runtime to remove unused
features.

This article only applies to Blazor WebAssembly.

.NET WebAssembly build tools


The .NET WebAssembly build tools are based on Emscripten , a compiler toolchain for
the web platform. For more information on the build tools, including installation, see
Tooling for ASP.NET Core Blazor.

Add native dependencies to a Blazor WebAssembly app by adding NativeFileReference


items in the app's project file. When the project is built, each NativeFileReference is
passed to Emscripten by the .NET WebAssembly build tools so that they are compiled
and linked into the runtime. Next, p/invoke into the native code from the app's .NET
code.

Generally, any portable native code can be used as a native dependency with Blazor
WebAssembly. You can add native dependencies to C/C++ code or code previously
compiled using Emscripten:

Object files ( .o )
Archive files ( .a )
Bitcode ( .bc )
Standalone WebAssembly modules ( .wasm )

Prebuilt dependencies typically must be built using the same version of Emscripten used
to build the .NET WebAssembly runtime.

7 Note

For Mono /WebAssembly MSBuild properties and targets, see WasmApp.targets


(dotnet/runtime GitHub repository) . Official documentation for common
MSBuild properties is planned per Document blazor msbuild configuration
options (dotnet/docs #27395) .

Use native code


Add a simple native C function to a Blazor WebAssembly app:

1. Create a new Blazor WebAssembly project.

2. Add a Test.c file to the project.

3. Add a C function for computing factorials.

Test.c :

int fact(int n)
{
if (n == 0) return 1;
return n * fact(n - 1);
}

4. Add a NativeFileReference for Test.c in the app's project file:

XML

<ItemGroup>
<NativeFileReference Include="Test.c" />
</ItemGroup>

5. In a Razor component, add a DllImportAttribute for the fact function in the


generated Test library and call the fact method from .NET code in the
component.

Pages/NativeCTest.razor :

razor

@page "/native-c-test"
@using System.Runtime.InteropServices

<PageTitle>Native C</PageTitle>

<h1>Native C Test</h1>
<p>
@@fact(3) result: @fact(3)
</p>

@code {
[DllImport("Test")]
static extern int fact(int n);
}

When you build the app with the .NET WebAssembly build tools installed, the native C
code is compiled and linked into the .NET WebAssembly runtime ( dotnet.wasm ). After
the app is built, run the app to see the rendered factorial value.

C++ managed method callbacks


Label managed methods that are passed to C++ with the [UnmanagedCallersOnly]
attribute.

The method marked with the [UnmanagedCallersOnly] attribute must be static . To call
an instance method in a Razor component, pass a GCHandle for the instance to C++ and
then pass it back to native. Alternatively, use some other method to identify the instance
of the component.

The method marked with [DllImport] must use a C# 9.0 function pointer rather than a
delegate type for the callback argument.

7 Note

For C# function pointer types in [DllImport] methods, use IntPtr in the method
signature on the managed side instead of delegate *unmanaged<int, void> . For
more information, see [WASM] callback from native code to .NET: Parsing
function pointer types in signatures is not supported (dotnet/runtime #56145) .

Package native dependencies in a NuGet


package
NuGet packages can contain native dependencies for use on WebAssembly. These
libraries and their native functionality are then available to any Blazor WebAssembly
app. The files for the native dependencies should be built for WebAssembly and
packaged in the browser-wasm architecture-specific folder. WebAssembly-specific
dependencies aren't referenced automatically and must be referenced manually as
NativeFileReference s. Package authors can choose to add the native references by
including a .props file in the package with the references.

SkiaSharp example library use


SkiaSharp is a cross-platform 2D graphics library for .NET based on the native Skia
graphics library with support for Blazor WebAssembly.

To use SkiaSharp in a Blazor WebAssembly app:

1. Add a package reference to the SkiaSharp.Views.Blazor package in a Blazor


WebAssembly project. Use Visual Studio's process for adding packages to an app
(Manage NuGet Packages with Include prerelease selected) or execute the dotnet
add package command in a command shell:

.NET CLI

dotnet add package –-prerelease SkiaSharp.Views.Blazor

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 .

2. Add a SKCanvasView component to the app with the following:

SkiaSharp and SkiaSharp.Views.Blazor namespaces.

Logic to draw in the SkiaSharp Canvas View component ( SKCanvasView ).

Pages/NativeDependencyExample.razor :

razor

@page "/native-dependency-example"
@using SkiaSharp
@using SkiaSharp.Views.Blazor

<PageTitle>Native dependency</PageTitle>

<h1>Native dependency example with SkiaSharp</h1>

<SKCanvasView OnPaintSurface="@OnPaintSurface" />


@code {
private void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;

canvas.Clear(SKColors.White);

using var paint = new SKPaint


{
Color = SKColors.Black,
IsAntialias = true,
TextSize = 24
};

canvas.DrawText("SkiaSharp", 0, 24, paint);


}
}

3. Build the app, which might take several minutes. Run the app and navigate to the
NativeDependencyExample component at /native-dependency-example .

Additional resources
.NET WebAssembly build tools
ASP.NET Core Blazor performance best
practices
Article • 04/04/2023

Blazor is optimized for high performance in most realistic application UI scenarios.


However, the best performance depends on developers adopting the correct patterns
and features.

Optimize rendering speed


Optimize rendering speed to minimize rendering workload and improve UI
responsiveness, which can yield a ten-fold or higher improvement in UI rendering speed.

Avoid unnecessary rendering of component subtrees


You might be able to remove the majority of a parent component's rendering cost by
skipping the rerendering of child component subtrees when an event occurs. You should
only be concerned about skipping the rerendering subtrees that are particularly
expensive to render and are causing UI lag.

At runtime, components exist in a hierarchy. A root component has child components.


In turn, the root's children have their own child components, and so on. When an event
occurs, such as a user selecting a button, the following process determines which
components to rerender:

1. The event is dispatched to the component that rendered the event's handler. After
executing the event handler, the component is rerendered.
2. When a component is rerendered, it supplies a new copy of parameter values to
each of its child components.
3. After a new set of parameter values is received, each component decides whether
to rerender. By default, components rerender if the parameter values may have
changed, for example, if they're mutable objects.

The last two steps of the preceding sequence continue recursively down the component
hierarchy. In many cases, the entire subtree is rerendered. Events targeting high-level
components can cause expensive rerendering because every component below the
high-level component must rerender.

To prevent rendering recursion into a particular subtree, use either of the following
approaches:
Ensure that child component parameters are of primitive immutable types, such as
string , int , bool , DateTime , and other similar types. The built-in logic for
detecting changes automatically skips rerendering if the primitive immutable
parameter values haven't changed. If you render a child component with <Customer
CustomerId="@item.CustomerId" /> , where CustomerId is an int type, then the

Customer component isn't rerendered unless item.CustomerId changes.

Override ShouldRender:
To accept nonprimitive parameter values, such as complex custom model types,
event callbacks, or RenderFragment values.
If authoring a UI-only component that doesn't change after the initial render,
regardless of parameter value changes.

The following airline flight search tool example uses private fields to track the necessary
information to detect changes. The previous inbound flight identifier
( prevInboundFlightId ) and previous outbound flight identifier ( prevOutboundFlightId )
track information for the next potential component update. If either of the flight
identifiers change when the component's parameters are set in OnParametersSet, the
component is rerendered because shouldRender is set to true . If shouldRender
evaluates to false after checking the flight identifiers, an expensive rerender is avoided:

razor

@code {
private int prevInboundFlightId = 0;
private int prevOutboundFlightId = 0;
private bool shouldRender;

[Parameter]
public FlightInfo? InboundFlight { get; set; }

[Parameter]
public FlightInfo? OutboundFlight { get; set; }

protected override void OnParametersSet()


{
shouldRender = InboundFlight?.FlightId != prevInboundFlightId
|| OutboundFlight?.FlightId != prevOutboundFlightId;

prevInboundFlightId = InboundFlight?.FlightId ?? 0;
prevOutboundFlightId = OutboundFlight?.FlightId ?? 0;
}

protected override bool ShouldRender() => shouldRender;


}
An event handler can also set shouldRender to true . For most components, determining
rerendering at the level of individual event handlers usually isn't necessary.

For more information, see the following resources:

ASP.NET Core Razor component lifecycle


ShouldRender
ASP.NET Core Razor component rendering

Virtualization
When rendering large amounts of UI within a loop, for example, a list or grid with
thousands of entries, the sheer quantity of rendering operations can lead to a lag in UI
rendering. Given that the user can only see a small number of elements at once without
scrolling, it's often wasteful to spend time rendering elements that aren't currently
visible.

Blazor provides the Virtualize component to create the appearance and scroll
behaviors of an arbitrarily-large list while only rendering the list items that are within the
current scroll viewport. For example, a component can render a list with 100,000 entries
but only pay the rendering cost of 20 items that are visible.

For more information, see ASP.NET Core Razor component virtualization.

Create lightweight, optimized components


Most Razor components don't require aggressive optimization efforts because most
components don't repeat in the UI and don't rerender at high frequency. For example,
routable components with an @page directive and components used to render high-
level pieces of the UI, such as dialogs or forms, most likely appear only one at a time
and only rerender in response to a user gesture. These components don't usually create
high rendering workload, so you can freely use any combination of framework features
without much concern about rendering performance.

However, there are common scenarios where components are repeated at scale and
often result in poor UI performance:

Large nested forms with hundreds of individual elements, such as inputs or labels.
Grids with hundreds of rows or thousands of cells.
Scatter plots with millions of data points.

If modelling each element, cell, or data point as a separate component instance, there
are often so many of them that their rendering performance becomes critical. This
section provides advice on making such components lightweight so that the UI remains
fast and responsive.

Avoid thousands of component instances

Each component is a separate island that can render independently of its parents and
children. By choosing how to split the UI into a hierarchy of components, you are taking
control over the granularity of UI rendering. This can result in either good or poor
performance.

By splitting the UI into separate components, you can have smaller portions of the UI
rerender when events occur. In a table with many rows that have a button in each row,
you may be able to have only that single row rerender by using a child component
instead of the whole page or table. However, each component requires additional
memory and CPU overhead to deal with its independent state and rendering lifecycle.

In a test performed by the ASP.NET Core product unit engineers, a rendering overhead
of around 0.06 ms per component instance was seen in a Blazor WebAssembly app. The
test app rendered a simple component that accepts three parameters. Internally, the
overhead is largely due to retrieving per-component state from dictionaries and passing
and receiving parameters. By multiplication, you can see that adding 2,000 extra
component instances would add 0.12 seconds to the rendering time and the UI would
begin feeling slow to users.

It's possible to make components more lightweight so that you can have more of them.
However, a more powerful technique is often to avoid having so many components to
render. The following sections describe two approaches that you can take.

Inline child components into their parents

Consider the following portion of a parent component that renders child components in
a loop:

razor

<div class="chat">
@foreach (var message in messages)
{
<ChatMessageDisplay Message="@message" />
}
</div>

Shared/ChatMessageDisplay.razor :
razor

<div class="chat-message">
<span class="author">@Message.Author</span>
<span class="text">@Message.Text</span>
</div>

@code {
[Parameter]
public ChatMessage? Message { get; set; }
}

The preceding example performs well if thousands of messages aren't shown at once. To
show thousands of messages at once, consider not factoring out the separate
ChatMessageDisplay component. Instead, inline the child component into the parent.
The following approach avoids the per-component overhead of rendering so many child
components at the cost of losing the ability to rerender each child component's markup
independently:

razor

<div class="chat">
@foreach (var message in messages)
{
<div class="chat-message">
<span class="author">@message.Author</span>
<span class="text">@message.Text</span>
</div>
}
</div>

Define reusable RenderFragments in code

You might be factoring out child components purely as a way of reusing rendering logic.
If that's the case, you can create reusable rendering logic without implementing
additional components. In any component's @code block, define a RenderFragment.
Render the fragment from any location as many times as needed:

razor

@RenderWelcomeInfo

<p>Render the welcome content a second time:</p>

@RenderWelcomeInfo

@code {
private RenderFragment RenderWelcomeInfo = @<p>Welcome to your new app!
</p>;
}

To make RenderTreeBuilder code reusable across multiple components, declare the


RenderFragment public and static:

razor

public static RenderFragment SayHello = @<h1>Hello!</h1>;

SayHello in the preceding example can be invoked from an unrelated component. This

technique is useful for building libraries of reusable markup snippets that render without
per-component overhead.

RenderFragment delegates can accept parameters. The following component passes the
message ( message ) to the RenderFragment delegate:

razor

<div class="chat">
@foreach (var message in messages)
{
@ChatMessageDisplay(message)
}
</div>

@code {
private RenderFragment<ChatMessage> ChatMessageDisplay = message =>
@<div class="chat-message">
<span class="author">@message.Author</span>
<span class="text">@message.Text</span>
</div>;
}

The preceding approach reuses rendering logic without per-component overhead.


However, the approach doesn't permit refreshing the subtree of the UI independently,
nor does it have the ability to skip rendering the subtree of the UI when its parent
renders because there's no component boundary. Assignment to a RenderFragment
delegate is only supported in Razor component files ( .razor ), and event callbacks aren't
supported.

For a non-static field, method, or property that can't be referenced by a field initializer,
such as TitleTemplate in the following example, use a property instead of a field for the
RenderFragment:
C#

protected RenderFragment DisplayTitle =>


@<div>
@TitleTemplate
</div>;

Don't receive too many parameters


If a component repeats extremely often, for example, hundreds or thousands of times,
the overhead of passing and receiving each parameter builds up.

It's rare that too many parameters severely restricts performance, but it can be a factor.
For a TableCell component that renders 1,000 times within a grid, each extra parameter
passed to the component could add around 15 ms to the total rendering cost. If each
cell accepted 10 parameters, parameter passing would take around 150 ms per
component for a total rendering cost of 150,000 ms (150 seconds) and cause a UI
rendering lag.

To reduce parameter load, bundle multiple parameters in a custom class. For example, a
table cell component might accept a common object. In the following example, Data is
different for every cell, but Options is common across all cell instances:

razor

@typeparam TItem

...

@code {
[Parameter]
public TItem? Data { get; set; }

[Parameter]
public GridOptions? Options { get; set; }
}

However, consider that it might be an improvement not to have a table cell component,
as shown in the preceding example, and instead inline its logic into the parent
component.

7 Note

When multiple approaches are available for improving performance, benchmarking


the approaches is usually required to determine which approach yields the best
results.

For more information on generic type parameters ( @typeparam ), see the following
resources:

Razor syntax reference for ASP.NET Core


ASP.NET Core Razor components
ASP.NET Core Blazor templated components

Ensure cascading parameters are fixed


The CascadingValue component has an optional IsFixed parameter:

If IsFixed is false (the default), every recipient of the cascaded value sets up a
subscription to receive change notifications. Each [CascadingParameter] is
substantially more expensive than a regular [Parameter] due to the subscription
tracking.
If IsFixed is true (for example, <CascadingValue Value="@someValue"
IsFixed="true"> ), recipients receive the initial value but don't set up a subscription
to receive updates. Each [CascadingParameter] is lightweight and no more
expensive than a regular [Parameter] .

Setting IsFixed to true improves performance if there are a large number of other
components that receive the cascaded value. Wherever possible, set IsFixed to true on
cascaded values. You can set IsFixed to true when the supplied value doesn't change
over time.

Where a component passes this as a cascaded value, IsFixed can also be set to true :

razor

<CascadingValue Value="this" IsFixed="true">


<SomeOtherComponents>
</CascadingValue>

For more information, see ASP.NET Core Blazor cascading values and parameters.

Avoid attribute splatting with CaptureUnmatchedValues


Components can elect to receive "unmatched" parameter values using the
CaptureUnmatchedValues flag:
razor

<div @attributes="OtherAttributes">...</div>

@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? OtherAttributes { get; set; }
}

This approach allows passing arbitrary additional attributes to the element. However,
this approach is expensive because the renderer must:

Match all of the supplied parameters against the set of known parameters to build
a dictionary.
Keep track of how multiple copies of the same attribute overwrite each other.

Use CaptureUnmatchedValues where component rendering performance isn't critical,


such as components that aren't repeated frequently. For components that render at
scale, such as each item in a large list or in the cells of a grid, try to avoid attribute
splatting.

For more information, see ASP.NET Core Blazor attribute splatting and arbitrary
parameters.

Implement SetParametersAsync manually

A significant source of per-component rendering overhead is writing incoming


parameter values to [Parameter] properties. The renderer uses reflection to write the
parameter values, which can lead to poor performance at scale.

In some extreme cases, you may wish to avoid the reflection and implement your own
parameter-setting logic manually. This may be applicable when:

A component renders extremely often, for example, when there are hundreds or
thousands of copies of the component in the UI.
A component accepts many parameters.
You find that the overhead of receiving parameters has an observable impact on UI
responsiveness.

In extreme cases, you can override the component's virtual SetParametersAsync method
and implement your own component-specific logic. The following example deliberately
avoids dictionary lookups:

razor
@code {
[Parameter]
public int MessageId { get; set; }

[Parameter]
public string? Text { get; set; }

[Parameter]
public EventCallback<string> TextChanged { get; set; }

[Parameter]
public Theme CurrentTheme { get; set; }

public override Task SetParametersAsync(ParameterView parameters)


{
foreach (var parameter in parameters)
{
switch (parameter.Name)
{
case nameof(MessageId):
MessageId = (int)parameter.Value;
break;
case nameof(Text):
Text = (string)parameter.Value;
break;
case nameof(TextChanged):
TextChanged = (EventCallback<string>)parameter.Value;
break;
case nameof(CurrentTheme):
CurrentTheme = (Theme)parameter.Value;
break;
default:
throw new ArgumentException($"Unknown parameter:
{parameter.Name}");
}
}

return base.SetParametersAsync(ParameterView.Empty);
}
}

In the preceding code, returning the base class SetParametersAsync runs the normal
lifecycle methods without assigning parameters again.

As you can see in the preceding code, overriding SetParametersAsync and supplying
custom logic is complicated and laborious, so we don't generally recommend adopting
this approach. In extreme cases, it can improve rendering performance by 20-25%, but
you should only consider this approach in the extreme scenarios listed earlier in this
section.
Don't trigger events too rapidly
Some browser events fire extremely frequently. For example, onmousemove and onscroll
can fire tens or hundreds of times per second. In most cases, you don't need to perform
UI updates this frequently. If events are triggered too rapidly, you may harm UI
responsiveness or consume excessive CPU time.

Rather than use native events that rapidly fire, consider the use of JS interop to register
a callback that fires less frequently. For example, the following component displays the
position of the mouse but only updates at most once every 500 ms:

razor

@inject IJSRuntime JS
@implements IDisposable

<h1>@message</h1>

<div @ref="mouseMoveElement" style="border:1px dashed red;height:200px;">


Move mouse here
</div>

@code {
private ElementReference mouseMoveElement;
private DotNetObjectReference<MyComponent>? selfReference;
private string message = "Move the mouse in the box";

[JSInvokable]
public void HandleMouseMove(int x, int y)
{
message = $"Mouse move at {x}, {y}";
StateHasChanged();
}

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
selfReference = DotNetObjectReference.Create(this);
var minInterval = 500;

await JS.InvokeVoidAsync("onThrottledMouseMove",
mouseMoveElement, selfReference, minInterval);
}
}

public void Dispose() => selfReference?.Dispose();


}
The corresponding JavaScript code registers the DOM event listener for mouse
movement. In this example, the event listener uses Lodash's throttle function to limit
the rate of invocations:

HTML

<script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"
></script>
<script>
function onThrottledMouseMove(elem, component, interval) {
elem.addEventListener('mousemove', _.throttle(e => {
component.invokeMethodAsync('HandleMouseMove', e.offsetX, e.offsetY);
}, interval));
}
</script>

Avoid rerendering after handling events without state


changes
By default, components inherit from ComponentBase, which automatically invokes
StateHasChanged after the component's event handlers are invoked. In some cases, it
might be unnecessary or undesirable to trigger a rerender after an event handler is
invoked. For example, an event handler might not modify component state. In these
scenarios, the app can leverage the IHandleEvent interface to control the behavior of
Blazor's event handling.

To prevent rerenders for all of a component's event handlers, implement IHandleEvent


and provide a IHandleEvent.HandleEventAsync task that invokes the event handler
without calling StateHasChanged.

In the following example, no event handler added to the component triggers a rerender,
so HandleSelect doesn't result in a rerender when invoked.

Pages/HandleSelect1.razor :

razor

@page "/handle-select-1"
@using Microsoft.Extensions.Logging
@implements IHandleEvent
@inject ILogger<HandleSelect1> Logger

<p>
Last render DateTime: @dt
</p>
<button @onclick="HandleSelect">
Select me (Avoids Rerender)
</button>

@code {
private DateTime dt = DateTime.Now;

private void HandleSelect()


{
dt = DateTime.Now;

Logger.LogInformation("This event handler doesn't trigger a


rerender.");
}

Task IHandleEvent.HandleEventAsync(
EventCallbackWorkItem callback, object? arg) =>
callback.InvokeAsync(arg);
}

In addition to preventing rerenders after event handlers fire in a component in a global


fashion, it's possible to prevent rerenders after a single event handler by employing the
following utility method.

Add the following EventUntil class to a Blazor app. The static actions and functions at
the top of the EventUtil class provide handlers that cover several combinations of
arguments and return types that Blazor uses when handling events.

EventUtil.cs :

C#

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

public static class EventUtil


{
public static Action AsNonRenderingEventHandler(Action callback)
=> new SyncReceiver(callback).Invoke;
public static Action<TValue> AsNonRenderingEventHandler<TValue>(
Action<TValue> callback)
=> new SyncReceiver<TValue>(callback).Invoke;
public static Func<Task> AsNonRenderingEventHandler(Func<Task> callback)
=> new AsyncReceiver(callback).Invoke;
public static Func<TValue, Task> AsNonRenderingEventHandler<TValue>(
Func<TValue, Task> callback)
=> new AsyncReceiver<TValue>(callback).Invoke;

private record SyncReceiver(Action callback)


: ReceiverBase { public void Invoke() => callback(); }
private record SyncReceiver<T>(Action<T> callback)
: ReceiverBase { public void Invoke(T arg) => callback(arg); }
private record AsyncReceiver(Func<Task> callback)
: ReceiverBase { public Task Invoke() => callback(); }
private record AsyncReceiver<T>(Func<T, Task> callback)
: ReceiverBase { public Task Invoke(T arg) => callback(arg); }

private record ReceiverBase : IHandleEvent


{
public Task HandleEventAsync(EventCallbackWorkItem item, object arg)
=>
item.InvokeAsync(arg);
}
}

Call EventUtil.AsNonRenderingEventHandler to call an event handler that doesn't trigger


a render when invoked.

In the following example:

Selecting the first button, which calls HandleClick1 , triggers a rerender.


Selecting the second button, which calls HandleClick2 , doesn't trigger a rerender.
Selecting the third button, which calls HandleClick3 , doesn't trigger a rerender and
uses event arguments (MouseEventArgs).

Pages/HandleSelect2.razor :

razor

@page "/handle-select-2"
@using Microsoft.Extensions.Logging
@inject ILogger<HandleSelect2> Logger

<p>
Last render DateTime: @dt
</p>

<button @onclick="HandleClick1">
Select me (Rerenders)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler(HandleClick2)">
Select me (Avoids Rerender)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>
(HandleClick3)">
Select me (Avoids Rerender and uses <code>MouseEventArgs</code>)
</button>
@code {
private DateTime dt = DateTime.Now;

private void HandleClick1()


{
dt = DateTime.Now;

Logger.LogInformation("This event handler triggers a rerender.");


}

private void HandleClick2()


{
dt = DateTime.Now;

Logger.LogInformation("This event handler doesn't trigger a


rerender.");
}

private void HandleClick3(MouseEventArgs args)


{
dt = DateTime.Now;

Logger.LogInformation(
"This event handler doesn't trigger a rerender. " +
"Mouse coordinates: {ScreenX}:{ScreenY}",
args.ScreenX, args.ScreenY);
}
}

In addition to implementing the IHandleEvent interface, leveraging the other best


practices described in this article can also help reduce unwanted renders after events are
handled. For example, overriding ShouldRender in child components of the target
component can be used to control rerendering.

Avoid recreating delegates for many repeated elements


or components
Blazor's recreation of lambda expression delegates for elements or components in a
loop can lead to poor performance.

The following component shown in the event handling article renders a set of buttons.
Each button assigns a delegate to its @onclick event, which is fine if there aren't many
buttons to render:

razor

@page "/event-handler-example-5"

<h1>@heading</h1>
@for (var i = 1; i < 4; i++)
{
var buttonNumber = i;

<p>
<button @onclick="@(e => UpdateHeading(e, buttonNumber))">
Button #@i
</button>
</p>
}

@code {
private string heading = "Select a button to learn its position";

private void UpdateHeading(MouseEventArgs e, int buttonNumber)


{
heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
}
}

If a large number of buttons are rendered using the preceding approach, rendering
speed is adversely impacted leading to a poor user experience. To render a large
number of buttons with a callback for click events, the following example uses a
collection of button objects that assign each button's @onclick delegate to an Action.
The following approach doesn't require Blazor to rebuild all of the button delegates
each time the buttons are rendered:

Pages/LambdaEventPerformance.razor :

razor

@page "/lambda-event-performance"

<h1>@heading</h1>

@foreach (var button in Buttons)


{
<p>
<button @key="button.Id" @onclick="button.Action">
Button #@button.Id
</button>
</p>
}

@code {
private string heading = "Select a button to learn its position";

private List<Button> Buttons { get; set; } = new();

protected override void OnInitialized()


{
for (var i = 0; i < 100; i++)
{
var button = new Button();

button.Id = Guid.NewGuid().ToString();

button.Action = (e) =>


{
UpdateHeading(button, e);
};

Buttons.Add(button);
}
}

private void UpdateHeading(Button button, MouseEventArgs e)


{
heading = $"Selected #{button.Id} at {e.ClientX}:{e.ClientY}";
}

private class Button


{
public string? Id { get; set; }
public Action<MouseEventArgs> Action { get; set; } = e => { };
}
}

Optimize JavaScript interop speed


Calls between .NET and JavaScript require additional overhead because:

By default, calls are asynchronous.


By default, parameters and return values are JSON-serialized to provide an easy-
to-understand conversion mechanism between .NET and JavaScript types.

Additionally on Blazor Server, these calls are passed across the network.

Avoid excessively fine-grained calls


Since each call involves some overhead, it can be valuable to reduce the number of calls.
Consider the following code, which stores a collection of items in the browser's
localStorage :

C#

private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)


{
foreach (var item in items)
{
await JS.InvokeVoidAsync("localStorage.setItem", item.Id,
JsonSerializer.Serialize(item));
}
}

The preceding example makes a separate JS interop call for each item. Instead, the
following approach reduces the JS interop to a single call:

C#

private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)


{
await JS.InvokeVoidAsync("storeAllInLocalStorage", items);
}

The corresponding JavaScript function stores the whole collection of items on the client:

JavaScript

function storeAllInLocalStorage(items) {
items.forEach(item => {
localStorage.setItem(item.id, JSON.stringify(item));
});
}

For Blazor WebAssembly apps, rolling individual JS interop calls into a single call usually
only improves performance significantly if the component makes a large number of JS
interop calls.

Consider the use of synchronous calls

Call JavaScript from .NET

This section only applies to Blazor WebAssembly apps.

JS interop calls are asynchronous by default, regardless of whether the called code is
synchronous or asynchronous. Calls are asynchronous by default to ensure that
components are compatible across both Blazor hosting models, Blazor Server and Blazor
WebAssembly. On Blazor Server, all JS interop calls must be asynchronous because
they're sent over a network connection.

If you know for certain that your app only ever runs on Blazor WebAssembly, you can
choose to make synchronous JS interop calls. This has slightly less overhead than
making asynchronous calls and can result in fewer render cycles because there's no
intermediate state while awaiting results.

To make a synchronous call from .NET to JavaScript in a Blazor WebAssembly app, cast
IJSRuntime to IJSInProcessRuntime to make the JS interop call:

razor

@inject IJSRuntime JS

...

@code {
protected override void HandleSomeEvent()
{
var jsInProcess = (IJSInProcessRuntime)JS;
var value = jsInProcess.Invoke<string>
("javascriptFunctionIdentifier");
}
}

When working with IJSObjectReference in ASP.NET Core 5.0 or later Blazor


WebAssembly apps, you can use IJSInProcessObjectReference synchronously instead.
IJSInProcessObjectReference implements IAsyncDisposable/IDisposable and should be
disposed for garbage collection to prevent a memory leak, as the following example
demonstrates:

razor

...

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
...
private IJSInProcessObjectReference? module;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
module = await JS.InvokeAsync<IJSInProcessObjectReference>
("import",
"./scripts.js");
}
}
...

async ValueTask IAsyncDisposable.DisposeAsync()


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

Call .NET from JavaScript


This section only applies to Blazor WebAssembly apps.

JS interop calls are asynchronous by default, regardless of whether the called code is
synchronous or asynchronous. Calls are asynchronous by default to ensure that
components are compatible across both Blazor hosting models, Blazor Server and Blazor
WebAssembly. On Blazor Server, all JS interop calls must be asynchronous because
they're sent over a network connection.

If you know for certain that your app only ever runs on Blazor WebAssembly, you can
choose to make synchronous JS interop calls. This has slightly less overhead than
making asynchronous calls and can result in fewer render cycles because there's no
intermediate state while awaiting results.

To make a synchronous call from JavaScript to .NET in Blazor WebAssembly apps, use
DotNet.invokeMethod instead of DotNet.invokeMethodAsync .

Synchronous calls work if:

The app is running on Blazor WebAssembly, not Blazor Server.


The called function returns a value synchronously. The function isn't an async
method and doesn't return a .NET Task or JavaScript Promise .

Use JavaScript [JSImport] / [JSExport] interop


JavaScript [JSImport] / [JSExport] interop for Blazor WebAssembly apps offers
improved performance and stability over the JS interop API in framework releases prior
to ASP.NET Core 7.0.

For more information, see JavaScript JSImport/JSExport interop with ASP.NET Core
Blazor WebAssembly.
Ahead-of-time (AOT) compilation
Ahead-of-time (AOT) compilation compiles a Blazor app's .NET code directly into native
WebAssembly for direct execution by the browser. AOT-compiled apps result in larger
apps that take longer to download, but AOT-compiled apps usually provide better
runtime performance, especially for apps that execute CPU-intensive tasks. For more
information, see Host and deploy ASP.NET Core Blazor WebAssembly.

Minimize app download size

Runtime relinking
For information on how runtime relinking minimizes an app's download size, see Host
and deploy ASP.NET Core Blazor WebAssembly.

Use System.Text.Json
Blazor's JS interop implementation relies on System.Text.Json, which is a high-
performance JSON serialization library with low memory allocation. Using
System.Text.Json shouldn't result in additional app payload size over adding one or
more alternate JSON libraries.

For migration guidance, see How to migrate from Newtonsoft.Json to System.Text.Json.

Intermediate Language (IL) trimming


This section only applies to Blazor WebAssembly apps.

Trimming unused assemblies from a Blazor WebAssembly app reduces the app's size by
removing unused code in the app's binaries. For more information, see Configure the
Trimmer for ASP.NET Core Blazor.

Lazy load assemblies


This section only applies to Blazor WebAssembly apps.

Load assemblies at runtime when the assemblies are required by a route. For more
information, see Lazy load assemblies in ASP.NET Core Blazor WebAssembly.

Compression
This section only applies to Blazor WebAssembly apps.

When a Blazor WebAssembly app is published, the output is statically compressed


during publish to reduce the app's size and remove the overhead for runtime
compression. Blazor relies on the server to perform content negotiation and serve
statically-compressed files.

After an app is deployed, verify that the app serves compressed files. Inspect the
Network tab in a browser's developer tools and verify that the files are served with
Content-Encoding: br (Brotli compression) or Content-Encoding: gz (Gzip compression).

If the host isn't serving compressed files, follow the instructions in Host and deploy
ASP.NET Core Blazor WebAssembly.

Disable unused features


This section only applies to Blazor WebAssembly apps.

Blazor WebAssembly's runtime includes the following .NET features that can be disabled
for a smaller payload size:

A data file is included to make timezone information correct. If the app doesn't
require this feature, consider disabling it by setting the
BlazorEnableTimeZoneSupport MSBuild property in the app's project file to false :

XML

<PropertyGroup>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
</PropertyGroup>

By default, Blazor WebAssembly carries globalization resources required to display


values, such as dates and currency, in the user's culture. If the app doesn't require
localization, you may configure the app to support the invariant culture, which is
based on the en-US culture.
Test Razor components in ASP.NET Core
Blazor
Article • 04/04/2023

By: Egil Hansen

Testing Razor components is an important aspect of releasing stable and maintainable


Blazor apps.

To test a Razor component, the component under test (CUT) is:

Rendered with relevant input for the test.


Depending on the type of test performed, possibly subject to interaction or
modification. For example, event handlers can be triggered, such as an onclick
event for a button.
Inspected for expected values. A test passes when one or more inspected values
matches the expected values for the test.

Test approaches
Two common approaches for testing Razor components are end-to-end (E2E) testing
and unit testing:

Unit testing: Unit tests are written with a unit testing library that provides:
Component rendering.
Inspection of component output and state.
Triggering of event handlers and life cycle methods.
Assertions that component behavior is correct.

bUnit is an example of a library that enables Razor component unit testing.

E2E testing: A test runner runs a Blazor app containing the CUT and automates a
browser instance. The testing tool inspects and interacts with the CUT through the
browser. Playwright for .NET is an example of an E2E testing framework that can
be used with Blazor apps.

In unit testing, only the Razor component (Razor/C#) is involved. External dependencies,
such as services and JS interop, must be mocked. In E2E testing, the Razor component
and all of its auxiliary infrastructure are part of the test, including CSS, JS, and the DOM
and browser APIs.
Test scope describes how extensive the tests are. Test scope typically has an influence on
the speed of the tests. Unit tests run on a subset of the app's subsystems and usually
execute in milliseconds. E2E tests, which test a broad group of the app's subsystems, can
take several seconds to complete.

Unit testing also provides access to the instance of the CUT, allowing for inspection and
verification of the component's internal state. This normally isn't possible in E2E testing.

With regard to the component's environment, E2E tests must make sure that the
expected environmental state has been reached before verification starts. Otherwise, the
result is unpredictable. In unit testing, the rendering of the CUT and the life cycle of the
test are more integrated, which improves test stability.

E2E testing involves launching multiple processes, network and disk I/O, and other
subsystem activity that often lead to poor test reliability. Unit tests are typically insulated
from these sorts of issues.

The following table summarizes the difference between the two testing approaches.

Capability Unit testing E2E testing

Test scope Razor component Razor component (Razor/C#) with


(Razor/C#) only CSS/JS

Test execution time Milliseconds Seconds

Access to the component Yes No


instance

Sensitive to the environment No Yes

Reliability More reliable Less reliable

Choose the most appropriate test approach


Consider the scenario when choosing the type of testing to perform. Some
considerations are described in the following table.

Scenario Suggested Remarks


approach

Component Unit When there's no dependency on JS interop in a Razor component,


without JS testing the component can be tested without access to JS or the DOM API.
interop logic In this scenario, there are no disadvantages to choosing unit
testing.
Scenario Suggested Remarks
approach

Component Unit It's common for components to query the DOM or trigger
with simple JS testing animations through JS interop. Unit testing is usually preferred in
interop logic this scenario, since it's straightforward to mock the JS interaction
through the IJSRuntime interface.

Component Unit If a component uses JS interop to call a large or complex JS library


that depends testing and but the interaction between the Razor component and JS library is
on complex separate JS simple, then the best approach is likely to treat the component and
JS code testing JS library or code as two separate parts and test each individually.
Test the Razor component with a unit testing library, and test the JS
with a JS testing library.

Component E2E testing When a component's functionality is dependent on JS and its


with logic manipulation of the DOM, verify both the JS and Blazor code
that depends together in an E2E test. This is the approach that the Blazor
on JS framework developers have taken with Blazor's browser rendering
manipulation logic, which has tightly-coupled C# and JS code. The C# and JS
of the code must work together to correctly render Razor components in
browser DOM a browser.

Component E2E testing When a component's functionality is dependent on a 3rd party


that depends class library that has hard-to-mock dependencies, such as JS
on 3rd party interop, E2E testing might be the only option to test the
class library component.
with hard-to-
mock
dependencies

Test components with bUnit


There's no official Microsoft testing framework for Blazor, but the community-driven
project bUnit provides a convenient way to unit test Razor components.

7 Note

bUnit is a third-party testing library and isn't supported or maintained by Microsoft.

bUnit works with general-purpose testing frameworks, such as MSTest, NUnit , and
xUnit . These testing frameworks make bUnit tests look and feel like regular unit tests.
bUnit tests integrated with a general-purpose testing framework are ordinarily executed
with:

Visual Studio's Test Explorer.


dotnet test CLI command in a command shell.
An automated DevOps testing pipeline.

7 Note

Test concepts and test implementations across different test frameworks are similar
but not identical. Refer to the test framework's documentation for guidance.

The following demonstrates the structure of a bUnit test on the Counter component in
an app based on a Blazor project template. The Counter component displays and
increments a counter based on the user selecting a button in the page:

razor

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

The following bUnit test verifies that the CUT's counter is incremented correctly when
the button is selected:

C#

[Fact]
public void CounterShouldIncrementWhenSelected()
{
// Arrange
using var ctx = new TestContext();
var cut = ctx.RenderComponent<Counter>();
var paraElm = cut.Find("p");

// Act
cut.Find("button").Click();
var paraElmText = paraElm.TextContent;
// Assert
paraElmText.MarkupMatches("Current count: 1");
}

The following actions take place at each step of the test:

Arrange: The Counter component is rendered using bUnit's TestContext . The


CUT's paragraph element ( <p> ) is found and assigned to paraElm .

Act: The button's element ( <button> ) is located and then selected by calling Click ,
which should increment the counter and update the content of the paragraph tag
( <p> ). The paragraph element text content is obtained by calling TextContent .

Assert: MarkupMatches is called on the text content to verify that it matches the
expected string, which is Current count: 1 .

7 Note

The MarkupMatches assert method differs from a regular string comparison


assertion (for example, Assert.Equal("Current count: 1", paraElmText); )
MarkupMatches performs a semantic comparison of the input and expected HTML

markup. A semantic comparison is aware of HTML semantics, meaning things like


insignificant whitespace is ignored. This results in more stable tests. For more
information, see Customizing the Semantic HTML Comparison .

Additional resources
Getting Started with bUnit : bUnit instructions include guidance on creating a test
project, referencing testing framework packages, and building and running tests.
ASP.NET Core Blazor Progressive Web
Application (PWA)
Article • 03/08/2023

A Blazor Progressive Web Application (PWA) is a single-page application (SPA) that uses
modern browser APIs and capabilities to behave like a desktop app.

Blazor WebAssembly is a standards-based client-side web app platform, so it can use


any browser API, including PWA APIs required for the following capabilities:

Working offline and loading instantly, independent of network speed.


Running in its own app window, not just a browser window.
Being launched from the host's operating system start menu, dock, or home
screen.
Receiving push notifications from a backend server, even while the user isn't using
the app.
Automatically updating in the background.

The word progressive is used to describe these apps because:

A user might first discover and use the app within their web browser like any other
SPA.
Later, the user progresses to installing it in their OS and enabling push
notifications.

Create a project from the PWA template


Visual Studio

When creating a new Blazor WebAssembly App, select the Progressive Web
Application checkbox.

Optionally, PWA can be configured for an app created from the ASP.NET Core Hosted
Blazor WebAssembly project template. The PWA scenario is independent of the hosting
model.

Convert an existing Blazor WebAssembly app


into a PWA
Convert an existing Blazor WebAssembly app into a PWA following the guidance in this
section.

In the app's project file:

Add the following ServiceWorkerAssetsManifest property to a PropertyGroup :

XML

...
<ServiceWorkerAssetsManifest>service-worker-
assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>

Add the following ServiceWorker item to an ItemGroup :

XML

<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js"
PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>

To obtain static assets, use one of the following approaches:

Create a separate, new PWA project with the dotnet new command in a command
shell:

.NET CLI

dotnet new blazorwasm -o MyBlazorPwa --pwa

In the preceding command, the -o|--output option creates a new folder for the
app named MyBlazorPwa .

If you aren't converting an app for the latest release, pass the -f|--framework
option. The following example creates the app for ASP.NET Core version 5.0:

.NET CLI

dotnet new blazorwasm -o MyBlazorPwa --pwa -f net5.0

Navigate to the ASP.NET Core GitHub repository at the following URL, which links
to main branch reference source and assets. Select the release that you're working
with from the Switch branches or tags dropdown list that applies to your app.
Blazor WebAssembly project template wwwroot folder (dotnet/aspnetcore GitHub
repository main branch)

7 Note

Documentation links to .NET reference source usually load the repository's


default branch, which represents the current development for the next release
of .NET. To select a tag for a specific release, use the Switch branches or tags
dropdown list. For more information, see How to select a version tag of
ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) .

From the source wwwroot folder either in the app that you created or from the reference
assets in the dotnet/aspnetcore GitHub repository, copy the following files into the
app's wwwroot folder:

icon-512.png
manifest.json

service-worker.js
service-worker.published.js

In the app's wwwroot/index.html file:

Add <link> elements for the manifest and app icon:

HTML

<link href="manifest.json" rel="manifest" />


<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />

Add the following <script> tag inside the closing </body> tag immediately after
the blazor.webassembly.js script tag:

HTML

...
<script>navigator.serviceWorker.register('service-worker.js');
</script>
</body>

Installation and app manifest


When visiting an app created using the PWA template, users have the option of
installing the app into their OS's start menu, dock, or home screen. The way this option
is presented depends on the user's browser. When using desktop Chromium-based
browsers, such as Edge or Chrome, an Add button appears within the URL bar. After the
user selects the Add button, they receive a confirmation dialog:

On iOS, visitors can install the PWA using Safari's Share button and its Add to
Homescreen option. On Chrome for Android, users should select the Menu button in
the upper-right corner, followed by Add to Home screen.

Once installed, the app appears in its own window without an address bar:
To customize the window's title, color scheme, icon, or other details, see the
manifest.json file in the project's wwwroot directory. The schema of this file is defined

by web standards. For more information, see MDN web docs: Web App Manifest .

Offline support
By default, apps created using the PWA template option have support for running
offline. A user must first visit the app while they're online. The browser automatically
downloads and caches all of the resources required to operate offline.

) Important

Development support would interfere with the usual development cycle of making
changes and testing them. Therefore, offline support is only enabled for published
apps.

2 Warning

If you intend to distribute an offline-enabled PWA, there are several important


warnings and caveats. These scenarios are inherent to offline PWAs and not
specific to Blazor. Be sure to read and understand these caveats before making
assumptions about how your offline-enabled app works.
To see how offline support works:

1. Publish the app. For more information, see Host and deploy ASP.NET Core Blazor.

2. Deploy the app to a server that supports HTTPS, and access the app in a browser
at its secure HTTPS address.

3. Open the browser's dev tools and verify that a Service Worker is registered for the
host on the Application tab:

4. Reload the page and examine the Network tab. Service Worker or memory cache
are listed as the sources for all of the page's assets:

5. To verify that the browser isn't dependent on network access to load the app,
either:

Shut down the web server and see how the app continues to function
normally, which includes page reloads. Likewise, the app continues to
function normally when there's a slow network connection.
Instruct the browser to simulate offline mode in the Network tab:
Offline support using a service worker is a web standard, not specific to Blazor. For more
information on service workers, see MDN web docs: Service Worker API . To learn more
about common usage patterns for service workers, see Google Web: The Service Worker
Lifecycle .

Blazor's PWA template produces two service worker files:

wwwroot/service-worker.js , which is used during development.

wwwroot/service-worker.published.js , which is used after the app is published.

To share logic between the two service worker files, consider the following approach:

Add a third JavaScript file to hold the common logic.


Use self.importScripts to load the common logic into both service worker files.

Cache-first fetch strategy


The built-in service-worker.published.js service worker resolves requests using a
cache-first strategy. This means that the service worker prefers to return cached content,
regardless of whether the user has network access or newer content is available on the
server.

The cache-first strategy is valuable because:

It ensures reliability. Network access isn't a boolean state. A user isn't simply
online or offline:
The user's device may assume it's online, but the network might be so slow as
to be impractical to wait for.
The network might return invalid results for certain URLs, such as when there's a
captive WIFI portal that's currently blocking or redirecting certain requests.

This is why the browser's navigator.onLine API isn't reliable and shouldn't be
depended upon.

It ensures correctness. When building a cache of offline resources, the service


worker uses content hashing to guarantee it has fetched a complete and self-
consistent snapshot of resources at a single instant in time. This cache is then used
as an atomic unit. There's no point asking the network for newer resources, since
the only versions required are the ones already cached. Anything else risks
inconsistency and incompatibility (for example, trying to use versions of .NET
assemblies that weren't compiled together).

If you must prevent the browser from fetching service-worker-assets.js from its HTTP
cache, for example to resolve temporary integrity check failures when deploying a new
version of the service worker, update the service worker registration in
wwwroot/index.html with updateViaCache set to 'none':

HTML

<script>
navigator.serviceWorker.register('/service-worker.js', {updateViaCache:
'none'});
</script>

Background updates
As a mental model, you can think of an offline-first PWA as behaving like a mobile app
that can be installed. The app starts up immediately regardless of network connectivity,
but the installed app logic comes from a point-in-time snapshot that might not be the
latest version.

The Blazor PWA template produces apps that automatically try to update themselves in
the background whenever the user visits and has a working network connection. The
way this works is as follows:

During compilation, the project generates a service worker assets manifest. By


default, this is called service-worker-assets.js . The manifest lists all the static
resources that the app requires to function offline, such as .NET assemblies,
JavaScript files, and CSS, including their content hashes. The resource list is loaded
by the service worker so that it knows which resources to cache.
Each time the user visits the app, the browser re-requests service-worker.js and
service-worker-assets.js in the background. The files are compared byte-for-byte
with the existing installed service worker. If the server returns changed content for
either of these files, the service worker attempts to install a new version of itself.
When installing a new version of itself, the service worker creates a new, separate
cache for offline resources and starts populating the cache with resources listed in
service-worker-assets.js . This logic is implemented in the onInstall function
inside service-worker.published.js .
The process completes successfully when all of the resources are loaded without
error and all content hashes match. If successful, the new service worker enters a
waiting for activation state. As soon as the user closes the app (no remaining app
tabs or windows), the new service worker becomes active and is used for
subsequent app visits. The old service worker and its cache are deleted.
If the process doesn't complete successfully, the new service worker instance is
discarded. The update process is attempted again on the user's next visit, when
hopefully the client has a better network connection that can complete the
requests.

Customize this process by editing the service worker logic. None of the preceding
behavior is specific to Blazor but is merely the default experience provided by the PWA
template option. For more information, see MDN web docs: Service Worker API .

How requests are resolved


As described in the Cache-first fetch strategy section, the default service worker uses a
cache-first strategy, meaning that it tries to serve cached content when available. If there
is no content cached for a certain URL, for example when requesting data from a
backend API, the service worker falls back on a regular network request. The network
request succeeds if the server is reachable. This logic is implemented inside onFetch
function within service-worker.published.js .

If the app's Razor components rely on requesting data from backend APIs and you want
to provide a friendly user experience for failed requests due to network unavailability,
implement logic within the app's components. For example, use try/catch around
HttpClient requests.

Support server-rendered pages


Consider what happens when the user first navigates to a URL such as /counter or any
other deep link in the app. In these cases, you don't want to return content cached as
/counter , but instead need the browser to load the content cached as /index.html to

start up your Blazor WebAssembly app. These initial requests are known as navigation
requests, as opposed to:

subresource requests for images, stylesheets, or other files.

fetch/XHR requests for API data.

The default service worker contains special-case logic for navigation requests. The
service worker resolves the requests by returning the cached content for /index.html ,
regardless of the requested URL. This logic is implemented in the onFetch function
inside service-worker.published.js .

If your app has certain URLs that must return server-rendered HTML, and not serve
/index.html from the cache, then you need to edit the logic in your service worker. If all

URLs containing /Identity/ need to be handled as regular online-only requests to the


server, then modify service-worker.published.js onFetch logic. Locate the following
code:

JavaScript

const shouldServeIndexHtml = event.request.mode === 'navigate';

Change the code to the following:

JavaScript

const shouldServeIndexHtml = event.request.mode === 'navigate'


&& !event.request.url.includes('/Identity/');

If you don't do this, then regardless of network connectivity, the service worker
intercepts requests for such URLs and resolves them using /index.html .

Add additional endpoints for external authentication providers to the check. In the
following example, /signin-google for Google authentication is added to the check:

JavaScript

const shouldServeIndexHtml = event.request.mode === 'navigate'


&& !event.request.url.includes('/Identity/')
&& !event.request.url.includes('/signin-google');
No action is required for the Development environment, where content is always
fetched from the network.

Control asset caching


If your project defines the ServiceWorkerAssetsManifest MSBuild property, Blazor's build
tooling generates a service worker assets manifest with the specified name. The default
PWA template produces a project file containing the following property:

XML

<ServiceWorkerAssetsManifest>service-worker-
assets.js</ServiceWorkerAssetsManifest>

The file is placed in the wwwroot output directory, so the browser can retrieve this file by
requesting /service-worker-assets.js . To see the contents of this file, open
/bin/Debug/{TARGET FRAMEWORK}/wwwroot/service-worker-assets.js in a text editor.

However, don't edit the file, as it's regenerated on each build.

By default, this manifest lists:

Any Blazor-managed resources, such as .NET assemblies and the .NET


WebAssembly runtime files required to function offline.
All resources for publishing to the app's wwwroot directory, such as images,
stylesheets, and JavaScript files, including static web assets supplied by external
projects and NuGet packages.

You can control which of these resources are fetched and cached by the service worker
by editing the logic in onInstall in service-worker.published.js . By default, the service
worker fetches and caches files matching typical web file name extensions such as
.html , .css , .js , and .wasm , plus file types specific to Blazor WebAssembly ( .dll ,
.pdb ).

To include additional resources that aren't present in the app's wwwroot directory, define
extra MSBuild ItemGroup entries, as shown in the following example:

XML

<ItemGroup>
<ServiceWorkerAssetsManifestItem Include="MyDirectory\AnotherFile.json"
RelativePath="MyDirectory\AnotherFile.json"
AssetUrl="files/AnotherFile.json" />
</ItemGroup>
The AssetUrl metadata specifies the base-relative URL that the browser should use
when fetching the resource to cache. This can be independent of its original source file
name on disk.

) Important

Adding a ServiceWorkerAssetsManifestItem doesn't cause the file to be published


in the app's wwwroot directory. The publish output must be controlled separately.
The ServiceWorkerAssetsManifestItem only causes an additional entry to appear in
the service worker assets manifest.

Push notifications
Like any other PWA, a Blazor WebAssembly PWA can receive push notifications from a
backend server. The server can send push notifications at any time, even when the user
isn't actively using the app. For example, push notifications can be sent when a different
user performs a relevant action.

The mechanism for sending a push notification is entirely independent of Blazor


WebAssembly, since it's implemented by the backend server which can use any
technology. If you want to send push notifications from an ASP.NET Core server,
consider using a technique similar to the approach taken in the Blazing Pizza
workshop .

The mechanism for receiving and displaying a push notification on the client is also
independent of Blazor WebAssembly, since it's implemented in the service worker
JavaScript file. For an example, see the approach used in the Blazing Pizza workshop .

Caveats for offline PWAs


Not all apps should attempt to support offline use. Offline support adds significant
complexity, while not always being relevant for the use cases required.

Offline support is usually relevant only:

If the primary data store is local to the browser. For example, the approach is
relevant in an app with a UI for an IoT device that stores data in localStorage or
IndexedDB .
If the app performs a significant amount of work to fetch and cache the backend
API data relevant to each user so that they can navigate through the data offline. If
the app must support editing, a system for tracking changes and synchronizing
data with the backend must be built.
If the goal is to guarantee that the app loads immediately regardless of network
conditions. Implement a suitable user experience around backend API requests to
show the progress of requests and behave gracefully when requests fail due to
network unavailability.

Additionally, offline-capable PWAs must deal with a range of additional complications.


Developers should carefully familiarize themselves with the caveats in the following
sections.

Offline support only when published


During development you typically want to see each change reflected immediately in the
browser without going through a background update process. Therefore, Blazor's PWA
template enables offline support only when published.

When building an offline-capable app, it's not enough to test the app in the
Development environment. You must test the app in its published state to understand
how it responds to different network conditions.

Update completion after user navigation away from app


Updates don't complete until the user has navigated away from the app in all tabs. As
explained in the Background updates section, after you deploy an update to the app,
the browser fetches the updated service worker files to begin the update process.

What surprises many developers is that, even when this update completes, it doesn't
take effect until the user has navigated away in all tabs. It isn't sufficient to refresh the
tab displaying the app, even if it's the only tab displaying the app. Until your app is
completely closed, the new service worker remains in the waiting to activate status. This
isn't specific to Blazor, but rather is a standard web platform behavior.

This commonly troubles developers who are trying to test updates to their service
worker or offline cached resources. If you check in the browser's developer tools, you
may see something like the following:
For as long as the list of "clients," which are tabs or windows displaying your app, is
nonempty, the worker continues waiting. The reason service workers do this is to
guarantee consistency. Consistency means that all resources are fetched from the same
atomic cache.

When testing changes, you may find it convenient to click the "skipWaiting" link as
shown in the preceding screenshot, then reload the page. You can automate this for all
users by coding your service worker to skip the "waiting" phase and immediately
activate on update . If you skip the waiting phase, you're giving up the guarantee that
resources are always fetched consistently from the same cache instance.

Users may run any historical version of the app


Web developers habitually expect that users only run the latest deployed version of their
web app, since that's normal within the traditional web distribution model. However, an
offline-first PWA is more akin to a native mobile app, where users aren't necessarily
running the latest version.

As explained in the Background updates section, after you deploy an update to your
app, each existing user continues to use a previous version for at least one further visit
because the update occurs in the background and isn't activated until the user
thereafter navigates away. Plus, the previous version being used isn't necessarily the
previous one you deployed. The previous version can be any historical version,
depending on when the user last completed an update.
This can be an issue if the frontend and backend parts of your app require agreement
about the schema for API requests. You must not deploy backward-incompatible API
schema changes until you can be sure that all users have upgraded. Alternatively, block
users from using incompatible older versions of the app. This scenario requirement is
the same as for native mobile apps. If you deploy a breaking change in server APIs, the
client app is broken for users who haven't yet updated.

If possible, don't deploy breaking changes to your backend APIs. If you must do so,
consider using standard Service Worker APIs such as ServiceWorkerRegistration to
determine whether the app is up-to-date, and if not, to prevent usage.

Interference with server-rendered pages


As described in the Support server-rendered pages section, if you want to bypass the
service worker's behavior of returning /index.html contents for all navigation requests,
edit the logic in your service worker.

All service worker asset manifest contents are cached by


default
As described in the Control asset caching section, the file service-worker-assets.js is
generated during build and lists all assets the service worker should fetch and cache.

Since this list by default includes everything emitted to wwwroot , including content
supplied by external packages and projects, you must be careful not to put too much
content there. If the wwwroot directory contains millions of images, the service worker
tries to fetch and cache them all, consuming excessive bandwidth and most likely not
completing successfully.

Implement arbitrary logic to control which subset of the manifest's contents should be
fetched and cached by editing the onInstall function in service-worker.published.js .

Interaction with authentication


The PWA template can be used in conjunction with authentication. An offline-capable
PWA can also support authentication when the user has initial network connectivity.

When a user doesn't have network connectivity, they can't authenticate or obtain access
tokens. By default, attempting to visit the login page without network access results in a
"network error" message. You must design a UI flow that allows the user perform useful
tasks while offline without attempting to authenticate the user or obtain access tokens.
Alternatively, you can design the app to gracefully fail when the network isn't available.
If the app can't be designed to handle these scenarios, you might not want to enable
offline support.

When an app that's designed for online and offline use is online again:

The app might need to provision a new access token.


The app must detect if a different user is signed into the service so that it can
apply operations to the user's account that were made while they were offline.

To create an offline PWA app that interacts with authentication:

Replace the AccountClaimsPrincipalFactory<TAccount> with a factory that stores


the last signed-in user and uses the stored user when the app is offline.
Queue operations while the app is offline and apply them when the app returns
online.
During sign out, clear the stored user.

The CarChecker sample app demonstrates the preceding approaches. See the
following parts of the app:

OfflineAccountClaimsPrincipalFactory

( Client/Data/OfflineAccountClaimsPrincipalFactory.cs )
LocalVehiclesStore ( Client/Data/LocalVehiclesStore.cs )

LoginStatus component ( Client/Shared/LoginStatus.razor )

Additional resources
Troubleshoot integrity PowerShell script
SignalR cross-origin negotiation for authentication
Host and deploy ASP.NET Core Blazor
Article • 05/19/2023

This article explains how to host and deploy Blazor apps.

Publish the app


Apps are published for deployment in Release configuration.

7 Note

Publish a hosted Blazor WebAssembly solution from the Server project.

Visual Studio

1. Select the Publish {APPLICATION} command from the Build menu, where the
{APPLICATION} placeholder the app's name.
2. Select the publish target. To publish locally, select Folder.
3. Accept the default location in the Choose a folder field or specify a different
location. Select the Publish button.

Publishing the app triggers a restore of the project's dependencies and builds the
project before creating the assets for deployment. As part of the build process, unused
methods and assemblies are removed to reduce app download size and load times.

Publish locations:

Blazor WebAssembly
Standalone: The app is published into the /bin/Release/{TARGET
FRAMEWORK}/publish/wwwroot or bin\Release\{TARGET FRAMEWORK}\browser-
wasm\publish folder, depending on the version of the SDK used to publish the

app. To deploy the app as a static site, copy the contents of the wwwroot folder
to the static site host.
Hosted: The client Blazor WebAssembly app is published into the
/bin/Release/{TARGET FRAMEWORK}/publish/wwwroot folder of the server app,
along with any other static web assets of the client app. Deploy the contents of
the publish folder to the host.
Blazor Server: The app is published into the /bin/Release/{TARGET
FRAMEWORK}/publish folder. Deploy the contents of the publish folder to the host.

The assets in the folder are deployed to the web server. Deployment might be a manual
or automated process depending on the development tools in use.

IIS
To host a Blazor app in IIS, see the following resources:

IIS hosting
Publish an ASP.NET Core app to IIS
Host ASP.NET Core on Windows with IIS
Host and deploy ASP.NET Core Blazor Server: Blazor Server apps running on IIS,
including IIS with Azure Virtual Machines (VMs) running Windows OS and Azure
App Service.
Host and deploy ASP.NET Core Blazor WebAssembly: Includes additional guidance
for Blazor WebAssembly apps hosted on IIS, including static site hosting, custom
web.config files, URL rewriting, sub-apps, compression, and Azure Storage static

file hosting.
IIS sub-application hosting
Follow the guidance in the App base path section for the Blazor app prior to
publishing the app. The examples use an app base path of /CoolApp .
Follow the sub-application configuration guidance in Advanced configuration.
The sub-app's folder path under the root site becomes the virtual path of the
sub-app. For an app base path of /CoolApp , the Blazor app is placed in a folder
named CoolApp under the root site and the sub-app takes on a virtual path of
/CoolApp .

Sharing an app pool among ASP.NET Core apps isn't supported, including for Blazor
apps. Use one app pool per app when hosting with IIS, and avoid the use of IIS's virtual
directories for hosting multiple apps.

One or more Blazor WebAssembly apps hosted by an ASP.NET Core app, known as a
hosted Blazor WebAssembly solution, are supported for one app pool. However, we
don't recommend or support assigning a single app pool to multiple hosted Blazor
WebAssembly solutions or in sub-app hosting scenarios.

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

App base path


The app base path is the app's root URL path. Successful routing in Blazor apps requires
framework configuration for any root URL path that isn't at the default app base path / .

Consider the following ASP.NET Core app and Blazor sub-app:

The ASP.NET Core app is named MyApp :


The app physically resides at d:/MyApp .
Requests are received at https://www.contoso.com/{MYAPP RESOURCE} .
A Blazor app named CoolApp is a sub-app of MyApp :
The sub-app physically resides at d:/MyApp/CoolApp .
Requests are received at https://www.contoso.com/CoolApp/{COOLAPP RESOURCE} .

Without specifying additional configuration for CoolApp , the sub-app in this scenario
has no knowledge of where it resides on the server. For example, the app can't construct
correct relative URLs to its resources without knowing that it resides at the relative URL
path /CoolApp/ . This scenario also applies in various hosting and reverse proxy scenarios
when an app isn't hosted at a root URL path.

Background
An anchor tag's destination (href ) can be composed with either of two endpoints:

Absolute locations that include a scheme (defaults to the page's scheme if


omitted), host, port, and path or just a forward slash ( / ) followed by the path.

Examples: https://example.com/a/b/c or /a/b/c

Relative locations that contain just a path and do not start with a forward slash ( / ).
These are resolved relative to the current document URL or the <base> tag's value,
if specified.

Example: a/b/c

The presence of a trailing slash ( / ) in a configured app base path is significant to


compute the base path for URLs of the app. For example, https://example.com/a has a
base path of https://example.com/ , while https://example.com/a/ with a trailing slash
has a base path of https://example.com/a .

There are three sources of links that pertain to Blazor in ASP.NET Core apps:

URLs manually written in the _Host.cshtml file (Blazor Server), which if you are
rendering inside different documents should always be absolute.
URLs in Razor components ( .razor ) are typically relative, but are essentially also
manually written.
URLs in scripts, such as the Blazor scripts ( blazor.webassembly.js and
blazor.server.js ), which are relative to the document.

If you're rendering a Blazor app from different documents (for example, /Admin/B/C/
and /Admin/D/E/ ), you must take the app base path into account, or the base path is
different when the app renders in each document and the resources are fetched from
the wrong URLs.

There are two approaches to deal with the challenge of resolving relative links correctly:

Map the resources dynamically using the document they were rendered on as the
root.
Set a consistent base path for the document and map the resources under that
base path.

The first option is more complicated and isn't the most typical approach, as it makes
navigation different for each document. Consider the following example for rendering a
page /Something/Else :

Rendered under /Admin/B/C/ , the page is rendered with a path of


/Admin/B/C/Something/Else .

Rendered under /Admin/D/E/ , the page is rendered at the same path of


/Admin/B/C/Something/Else .

Under the first approach, routing offers IDynamicEndpointMetadata and MatcherPolicy,


which in combination can be the basis for implementing a completely dynamic solution
that determines at runtime about how requests are routed.

For the second option, which is the usual approach taken, the app sets the base path in
the document and maps the server endpoints to paths under the base. The following
guidance adopts this approach.

Blazor Server
Map the SignalR hub of a Blazor Server app by passing the path to MapBlazorHub,
which is the most typical approach:

C#

endpoints.MapBlazorHub("base/path");
The benefit of using MapBlazorHub is that you can map patterns, such as "{tenant}"
and not just concrete paths.

You can also map the SignalR hub when the app is in a virtual folder inside a forked
pipeline using a snippet similar to the following:

C#

app.Map("/base/path/", subapp => {


subapp.UsePathBase("/base/path/");
subapp.UseRouting();
subapp.UseEndpoints(endpoints => endpoints.MapBlazorHub());
});

Configure the <base> tag, per the guidance in the Configure the app base path section.

Hosted Blazor WebAssembly


If the app is a hosted Blazor WebAssembly app:

In the in the Server project ( Program.cs ):


Adjust the path of UseBlazorFrameworkFiles (for example,
app.UseBlazorFrameworkFiles("/base/path"); ).
Configure calls to UseStaticFiles (for example,
app.UseStaticFiles("/base/path"); ).
In the Client project:
Configure <StaticWebAssetBasePath> in the project file to match the path for
serving static web assets (for example,
<StaticWebAssetBasePath>base/path</StaticWebAssetBasePath> ).

Configure the <base> tag, per the guidance in the Configure the app base path
section.

For an example of hosting multiple Blazor WebAssembly apps in a hosted Blazor


WebAssembly solution, see Multiple hosted ASP.NET Core Blazor WebAssembly apps,
where approaches are explained for domain/port hosting and subpath hosting of
multiple Blazor WebAssembly client apps.

Standalone Blazor WebAssembly


In a standalone Blazor WebAssembly app, only the <base> tag is configured, per the
guidance in the Configure the app base path section.
Configure the app base path
To provide configuration for the Blazor app's base path of
https://www.contoso.com/CoolApp/ , set the app base path, which is also called the

relative root path.

By configuring the app base path, a component that isn't in the root directory can
construct URLs relative to the app's root path. Components at different levels of the
directory structure can build links to other resources at locations throughout the app.
The app base path is also used to intercept selected hyperlinks where the href target of
the link is within the app base path URI space. The Blazor router handles the internal
navigation.

In many hosting scenarios, the relative URL path to the app is the root of the app. In
these default cases, the app's relative URL base path is the following:

Blazor WebAssembly (standalone or hosted): / configured as <base href="/" /> .


Blazor Server: ~/ configured as <base href="~/" /> .

For the location of <head> content in Blazor apps, see ASP.NET Core Blazor project
structure.

7 Note

In some hosting scenarios, such as GitHub Pages and IIS sub-apps, the app base
path must be set to the server's relative URL path of the app.

Standalone Blazor WebAssembly ( wwwroot/index.html ):

HTML

<base href="/CoolApp/">

The trailing slash is required.

Hosted Blazor WebAssembly (Client project, wwwroot/index.html ):

HTML

<base href="/CoolApp/">

The trailing slash is required.


In the Server project, call UsePathBase first in the app's request processing
pipeline ( Program.cs ) immediately after the WebApplicationBuilder is built
( builder.Build() ) to configure the base path for any following middleware that
interacts with the request path:

C#

app.UsePathBase("/CoolApp");

In a Blazor Server app, use either of the following approaches:

Option 1: Use the <base> tag to set the app's base path (location of <head>
content):

HTML

<base href="/CoolApp/">

The trailing slash is required.

Option 2: Call UsePathBase first in the app's request processing pipeline


( Program.cs ) immediately after the WebApplicationBuilder is built
( builder.Build() ) to configure the base path for any following middleware that
interacts with the request path:

C#

app.UsePathBase("/CoolApp");

Calling UsePathBase is recommended when you also wish to run the Blazor
Server app locally. For example, supply the launch URL in
Properties/launchSettings.json :

XML

"launchUrl": "https://localhost:{PORT}/CoolApp",

The {PORT} placeholder in the preceding example is the port that matches the
secure port in the applicationUrl configuration path. The following example
shows the full launch profile for an app at port 7279:

XML
"BlazorSample": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7279;http://localhost:5279",
"launchUrl": "https://localhost:7279/CoolApp",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

For more information on the launchSettings.json file, see Use multiple


environments in ASP.NET Core. For additional information on Blazor app base
paths and hosting, see <base href="/" /> or base-tag alternative for Blazor MVC
integration (dotnet/aspnetcore #43191) .

7 Note

When using WebApplication (see Migrate from ASP.NET Core 5.0 to 6.0),
app.UseRouting must be called after UsePathBase so that the Routing Middleware
can observe the modified path before matching routes. Otherwise, routes are
matched before the path is rewritten by UsePathBase as described in the
Middleware Ordering and Routing articles.

Do not prefix links throughout the app with a forward slash. Either avoid the use of a
path segment separator or use dot-slash ( ./ ) relative path notation:

❌ Incorrect: <a href="/account">


✔️Correct: <a href="account">
✔️Correct: <a href="./account">

In Blazor WebAssembly web API requests with the HttpClient service, confirm that JSON
helpers (HttpClientJsonExtensions) do not prefix URLs with a forward slash ( / ):

❌ Incorrect: var rsp = await client.GetFromJsonAsync("/api/Account");


✔️Correct: var rsp = await client.GetFromJsonAsync("api/Account");

Do not prefix Navigation Manager relative links with a forward slash. Either avoid the
use of a path segment separator or use dot-slash ( ./ ) relative path notation
( Navigation is an injected NavigationManager):

❌ Incorrect: Navigation.NavigateTo("/other");
✔️Correct: Navigation.NavigateTo("other");
✔️Correct: Navigation.NavigateTo("./other");

In typical configurations for Azure/IIS hosting, additional configuration usually isn't


required. In some non-IIS hosting and reverse proxy hosting scenarios, additional Static
File Middleware configuration might be required:

To serve static files correctly (for example, app.UseStaticFiles("/CoolApp"); ).


To serve the Blazor script ( _framework/blazor.server.js or
_framework/blazor.webassembly.js ). For more information, see ASP.NET Core
Blazor static files.

For a Blazor WebAssembly app with a non-root relative URL path (for example, <base
href="/CoolApp/"> ), the app fails to find its resources when run locally. To overcome this

problem during local development and testing, you can supply a path base argument
that matches the href value of the <base> tag at runtime. Don't include a trailing slash.
To pass the path base argument when running the app locally, execute the dotnet run
command from the app's directory with the --pathbase option:

.NET CLI

dotnet run --pathbase=/{RELATIVE URL PATH (no trailing slash)}

For a Blazor WebAssembly app with a relative URL path of /CoolApp/ ( <base
href="/CoolApp/"> ), the command is:

.NET CLI

dotnet run --pathbase=/CoolApp

If you prefer to configure the app's launch profile to specify the pathbase automatically
instead of manually with dotnet run , set the commandLineArgs property in
Properties/launchSettings.json . The following also configures the launch URL
( launchUrl ):

JSON

"commandLineArgs": "--pathbase=/{RELATIVE URL PATH (no trailing slash)}",


"launchUrl": "{RELATIVE URL PATH (no trailing slash)}",

Using CoolApp as the example:

JSON
"commandLineArgs": "--pathbase=/CoolApp",
"launchUrl": "CoolApp",

Using either dotnet run with the --pathbase option or a launch profile configuration
that sets the base path, the Blazor WebAssembly app responds locally at
http://localhost:port/CoolApp .

For more information on the launchSettings.json file, see Use multiple environments in
ASP.NET Core. For additional information on Blazor app base paths and hosting, see
<base href="/" /> or base-tag alternative for Blazor MVC integration (dotnet/aspnetcore
#43191) .

Blazor Server MapFallbackToPage configuration


In scenarios where an app requires a separate area with custom resources and Razor
components:

Create a folder within the app's Pages folder to hold the resources. For example,
an administrator section of an app is created in a new folder named Admin
( Pages/Admin ).

Create a root page ( _Host.cshtml ) for the area. For example, create a
Pages/Admin/_Host.cshtml file from the app's main root page

( Pages/_Host.cshtml ). Don't provide an @page directive in the Admin _Host page.

Add a layout to the area's folder (for example, Pages/Admin/_Layout.razor ). In the


layout for the separate area, set the <base> tag href to match the area's folder
(for example, <base href="/Admin/" /> ). For demonstration purposes, add ~/ to
the static resources in the page. For example:
~/css/bootstrap/bootstrap.min.css
~/css/site.css

~/BlazorSample.styles.css (the example app's namespace is BlazorSample )


~/_framework/blazor.server.js (Blazor script)

If the area should have its own static asset folder, add the folder and specify its
location to Static File Middleware in Program.cs (for example,
app.UseStaticFiles("/Admin/wwwroot") ).

Razor components are added to the area's folder. At a minimum, add an Index
component to the area folder with the correct @page directive for the area. For
example, add a Pages/Admin/Index.razor file based on the app's default
Pages/Index.razor file. Indicate the Admin area as the route template at the top of

the file ( @page "/admin" ). Add additional components as needed. For example,
Pages/Admin/Component1.razor with an @page directive and route template of @page

"/admin/component1 .

In Program.cs , call MapFallbackToPage for the area's request path immediately


before the fallback root page path to the _Host page:

C#

...
app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("~/Admin/{*clientroutes:nonfile}",
"/Admin/_Host");
app.MapFallbackToPage("/_Host");

app.Run();

Host multiple Blazor WebAssembly apps


For more information on hosting multiple Blazor WebAssembly apps in a hosted Blazor
solution, see Multiple hosted ASP.NET Core Blazor WebAssembly apps.

Deployment
For deployment guidance, see the following topics:

Host and deploy ASP.NET Core Blazor WebAssembly


Host and deploy ASP.NET Core Blazor Server
Host and deploy Blazor Server
Article • 06/05/2023

This article explains how to host and deploy a Blazor Server app using ASP.NET Core.

Host configuration values


Blazor Server apps can accept Generic Host configuration values.

Deployment
Using the Blazor Server hosting model, Blazor is executed on the server from within an
ASP.NET Core app. UI updates, event handling, and JavaScript calls are handled over a
SignalR connection.

A web server capable of hosting an ASP.NET Core app is required. Visual Studio includes
the Blazor Server App project template ( blazorserver template when using the dotnet
new command). For more information on Blazor project templates, see ASP.NET Core
Blazor project structure.

Scalability
When considering the scalability of a single server (scale up), the memory available to an
app is likely the first resource that the app exhausts as user demands increase. The
available memory on the server affects the:

Number of active circuits that a server can support.


UI latency on the client.

For guidance on building secure and scalable Blazor server apps, see Threat mitigation
guidance for ASP.NET Core Blazor Server.

Each circuit uses approximately 250 KB of memory for a minimal Hello World-style app.
The size of a circuit depends on the app's code and the state maintenance requirements
associated with each component. We recommend that you measure resource demands
during development for your app and infrastructure, but the following baseline can be a
starting point in planning your deployment target: If you expect your app to support
5,000 concurrent users, consider budgeting at least 1.3 GB of server memory to the app
(or ~273 KB per user).
SignalR configuration
SignalR's hosting and scaling conditions apply to Blazor apps that use SignalR.

Transports
Blazor works best when using WebSockets as the SignalR transport due to lower latency,
better reliability, and improved security. Long Polling is used by SignalR when
WebSockets isn't available or when the app is explicitly configured to use Long Polling.
When deploying to Azure App Service, configure the app to use WebSockets in the
Azure portal settings for the service. For details on configuring the app for Azure App
Service, see the SignalR publishing guidelines.

A console warning appears if Long Polling is utilized:

Failed to connect via WebSockets, using the Long Polling fallback transport. This
may be due to a VPN or proxy blocking the connection.

Global deployment and connection failures


Recommendations for global deployments to geographical data centers:

Deploy the app to the regions where most of the users reside.
Take into consideration the increased latency for traffic across continents.
For Azure hosting, use the Azure SignalR Service.

If a deployed app frequently displays the reconnection UI due to ping timeouts caused
by Internet latency, lengthen the server and client timeouts:

Server

At least double the maximum roundtrip time expected between the client and the
server. Test, monitor, and revise the timeouts as needed. For the SignalR hub, set
the ClientTimeoutInterval (default: 30 seconds) and HandshakeTimeout (default: 15
seconds). The following example assumes that KeepAliveInterval uses the default
value of 15 seconds.

) Important

The KeepAliveInterval isn't directly related to the reconnection UI appearing.


The Keep-Alive interval doesn't necessarily need to be changed. If the
reconnection UI appearance issue is due to timeouts, the
ClientTimeoutInterval and HandshakeTimeout can be increased and the
Keep-Alive interval can remain the same. The important consideration is that if
you change the Keep-Alive interval, make sure that the client timeout value is
at least double the value of the Keep-Alive interval and that the Keep-Alive
interval on the client matches the server setting.

In the following example, the ClientTimeoutInterval is increased to 60


seconds, and the HandshakeTimeout is increased to 30 seconds.

For a Blazor Server app in Program.cs :

C#

builder.Services.AddServerSideBlazor()
.AddHubOptions(options =>
{
options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});

For more information, see ASP.NET Core Blazor SignalR guidance.

Client

Typically, double the value used for the server's KeepAliveInterval to set the timeout for
the client's server timeout ( serverTimeoutInMilliseconds or ServerTimeout, default: 30
seconds).

) Important

The Keep-Alive interval ( keepAliveIntervalInMilliseconds or KeepAliveInterval)


isn't directly related to the reconnection UI appearing. The Keep-Alive interval
doesn't necessarily need to be changed. If the reconnection UI appearance issue is
due to timeouts, the server timeout can be increased and the Keep-Alive interval
can remain the same. The important consideration is that if you change the Keep-
Alive interval, make sure that the timeout value is at least double the value of the
Keep-Alive interval and that the Keep-Alive interval on the server matches the client
setting.

In the following example, a custom value of 60 seconds is used for the server
timeout.

In Pages/_Host.cshtml of a Blazor Server app:


HTML

<script src="_framework/blazor.server.js" autostart="false"></script>


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

When creating a hub connection in a component, set the ServerTimeout (default: 30


seconds) and HandshakeTimeout (default: 15 seconds) on the built HubConnection.

The following example is based on the Index component in the SignalR with Blazor
tutorial. The server timeout is increased to 60 seconds, and the handshake timeout is
increased to 30 seconds:

C#

protected override async Task OnInitializedAsync()


{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.Build();

hubConnection.ServerTimeout = TimeSpan.FromSeconds(60);
hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);

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


...

await hubConnection.StartAsync();
}

When changing the values of the server timeout (ServerTimeout) or the Keep-Alive
interval (KeepAliveInterval:

The server timeout should be at least double the value assigned to the Keep-Alive
interval.
The Keep-Alive interval should be less than or equal to half the value assigned to
the server timeout.

For more information, see ASP.NET Core Blazor SignalR guidance.


Azure SignalR Service
We recommend using the Azure SignalR Service for Blazor Server apps. The service
works in conjunction with the app's Blazor Hub for scaling up a Blazor Server app to a
large number of concurrent SignalR connections. In addition, the SignalR Service's
global reach and high-performance data centers significantly aid in reducing latency
due to geography.

) Important

When WebSockets are disabled, Azure App Service simulates a real-time


connection using HTTP Long Polling. HTTP Long Polling is noticeably slower than
running with WebSockets enabled, which doesn't use polling to simulate a client-
server connection. In the event that Long Polling must be used, you may need to
configure the maximum poll interval ( MaxPollIntervalInSeconds ), which defines the
maximum poll interval allowed for Long Polling connections in Azure SignalR
Service if the service ever falls back from WebSockets to Long Polling. If the next
poll request does not come in within MaxPollIntervalInSeconds , Azure SignalR
Service cleans up the client connection. Note that Azure SignalR Service also cleans
up connections when cached waiting to write buffer size is greater than 1 MB to
ensure service performance. Default value for MaxPollIntervalInSeconds is 5
seconds. The setting is limited to 1-300 seconds.

We recommend using WebSockets for Blazor Server apps deployed to Azure App
Service. The Azure SignalR Service uses WebSockets by default. If the app doesn't
use the Azure SignalR Service, see Publish an ASP.NET Core SignalR app to Azure
App Service.

For more information, see:

What is Azure SignalR Service?


Performance guide for Azure SignalR Service
Publish an ASP.NET Core SignalR app to Azure App Service

Configuration
To configure an app for the Azure SignalR Service, the app must support sticky sessions,
where clients are redirected back to the same server when prerendering. The
ServerStickyMode option or configuration value is set to Required . Typically, an app
creates the configuration using one of the following approaches:
Program.cs :

C#

builder.Services.AddSignalR().AddAzureSignalR(options =>
{
options.ServerStickyMode =
Microsoft.Azure.SignalR.ServerStickyMode.Required;
});

Configuration (use one of the following approaches):

In appsettings.json :

JSON

"Azure:SignalR:ServerStickyMode": "Required"

The app service's Configuration > Application settings in the Azure portal
(Name: Azure__SignalR__ServerStickyMode , Value: Required ). This approach is
adopted for the app automatically if you provision the Azure SignalR Service.

7 Note

The following error is thrown by an app that hasn't enabled sticky sessions for
Azure SignalR Service:

blazor.server.js:1 Uncaught (in promise) Error: Invocation canceled due to the


underlying connection being closed.

Provision the Azure SignalR Service


To provision the Azure SignalR Service for an app in Visual Studio:

1. Create an Azure Apps publish profile in Visual Studio for the Blazor Server app.
2. Add the Azure SignalR Service dependency to the profile. If the Azure subscription
doesn't have a pre-existing Azure SignalR Service instance to assign to the app,
select Create a new Azure SignalR Service instance to provision a new service
instance.
3. Publish the app to Azure.
Provisioning the Azure SignalR Service in Visual Studio automatically enables sticky
sessions and adds the SignalR connection string to the app service's configuration.

Scalability on Azure Container Apps


Scaling Blazor Server apps on Azure Container Apps requires specific considerations in
addition to using the Azure SignalR Service. Due to the way request routing is handled,
the ASP.NET Core data protection service must be configured to persist keys in a
centralized location that all container instances can access. The keys can be stored in
Azure Blob Storage and protected with Azure Key Vault. The data protection service uses
the keys to deserialize Razor components.

7 Note

For a deeper exploration of this scenario and scaling container apps, see Scaling
ASP.NET Core Apps on Azure. The tutorial explains how to create and integrate the
services required to host apps on Azure Container Apps. Basic steps are also
provided in this section.

1. To configure the data protection service to use Azure Blob Storage and Azure Key
Vault, reference the following NuGet packages:

Azure.Identity : Provides classes to work with the Azure identity and access
management services.
Microsoft.Extensions.Azure : Provides helpful extension methods to perform
core Azure configurations.
Azure.Extensions.AspNetCore.DataProtection.Blobs : Allows storing ASP.NET
Core Data Protection keys in Azure Blob Storage so that keys can be shared
across several instances of a web app.
Azure.Extensions.AspNetCore.DataProtection.Keys : Enables protecting keys
at rest using the Azure Key Vault Key Encryption/Wrapping feature.

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 .

2. Update Program.cs with the following highlighted code:

C#
using Azure.Identity;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Azure;
var builder = WebApplication.CreateBuilder(args);
var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];

builder.Services.AddRazorPages();
builder.Services.AddHttpClient();
builder.Services.AddServerSideBlazor();

builder.Services.AddAzureClientsCore();

builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
new
DefaultAzureCredential())
.ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
new
DefaultAzureCredential());
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

The preceding changes allow the app to manage data protection using a
centralized, scalable architecture. DefaultAzureCredential discovers the container
app managed identity after the code is deployed to Azure and uses it to connect
to blob storage and the app's key vault.

3. To create the container app managed identity and grant it access to blob storage
and a key vault, complete the following steps:
a. In the Azure Portal, navigate to the overview page of the container app.
b. Select Service Connector from the left navigation.
c. Select + Create from the top navigation.
d. In the Create connection flyout menu, enter the following values:
Container: Select the container app you created to host your Blazor Server
app.
Service type: Select Blob Storage.
Subscription: Select the subscription that owns the container app.
Connection name: Enter a name of scalablerazorstorage .
Client type: Select .NET and then select Next.

e. Select System assigned managed identity and select Next.


f. Use the default network settings and select Next.
g. After Azure validates the settings, select Create.

Repeat the preceding settings for the key vault. Select the appropriate key vault
service and key in the Basics tab.

Azure App Service without the Azure SignalR


Service
When the Azure SignalR Service is not used, the App Service requires configuration for
Application Request Routing (ARR) affinity and WebSockets. Clients connect their
WebSockets directly to the app, not to the Azure SignalR Service.

Use the following guidance to configure the app:

Configure the app in Azure App Service.


App Service Plan Limits.

IIS
When using IIS, enable:

WebSockets on IIS.
Sticky sessions with Application Request Routing.

Kubernetes
Create an ingress definition with the following Kubernetes annotations for sticky
sessions :

YAML

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: <ingress-name>
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"

Linux with Nginx


Follow the guidance for an ASP.NET Core SignalR app with the following changes:

Change the location path from /hubroute ( location /hubroute { ... } ) to the
root path / ( location / { ... } ).
Remove the configuration for proxy buffering ( proxy_buffering off; ) because the
setting only applies to Server-Sent Events (SSE) , which aren't relevant to Blazor
app client-server interactions.

For more information and configuration guidance, consult the following resources:

ASP.NET Core SignalR production hosting and scaling


Host ASP.NET Core on Linux with Nginx
Configure ASP.NET Core to work with proxy servers and load balancers
NGINX as a WebSocket Proxy
WebSocket proxying
Consult developers on non-Microsoft support forums:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

Linux with Apache


To host a Blazor app behind Apache on Linux, configure ProxyPass for HTTP and
WebSockets traffic.

In the following example:

Kestrel server is running on the host machine.


The app listens for traffic on port 5000.
ProxyRequests On
ProxyPreserveHost On
ProxyPassMatch ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass /_blazor ws://localhost:5000/_blazor
ProxyPass / http://localhost:5000/
ProxyPassReverse / http://localhost:5000/

Enable the following modules:

a2enmod proxy
a2enmod proxy_wstunnel

Check the browser console for WebSockets errors. Example errors:

Firefox can't establish a connection to the server at ws://the-domain-


name.tld/_blazor?id=XXX
Error: Failed to start the transport 'WebSockets': Error: There was an error with the
transport.
Error: Failed to start the transport 'LongPolling': TypeError: this.transport is
undefined
Error: Unable to connect to the server with any of the available transports.
WebSockets failed
Error: Cannot send data if the connection is not in the 'Connected' State.

For more information and configuration guidance, consult the following resources:

Host ASP.NET Core on Linux with Apache


Configure ASP.NET Core to work with proxy servers and load balancers
Apache documentation
Consult developers on non-Microsoft support forums:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

Measure network latency


JS interop can be used to measure network latency, as the following example
demonstrates.

Shared/MeasureLatency.razor :
razor

@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)


{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
private DateTime startTime;
private TimeSpan? latency;

protected override async Task OnAfterRenderAsync(bool firstRender)


{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}

For a reasonable UI experience, we recommend a sustained UI latency of 250 ms or less.

Blazor Server memory model


Blazor Server creates a new circuit per user session. Each user session corresponds to
rendering a single document in the browser. For example, multiple tabs create multiple
sessions.

Blazor Server maintains a constant connection to the browser, called a circuit, that
initiated the session. Connections can be lost at any time for any of several reasons, such
as when the user loses network connectivity or abruptly closes the browser. When a
connection is lost, Blazor has a recovery mechanism that places a limited number of
circuits in a "disconnected" pool, giving clients a limited amount of time to reconnect
and re-establish the session (default: 3 minutes).

After that time, Blazor releases the circuit and discards the session. From that point on,
the circuit is eligible for garbage collection (GC) and is claimed when a collection for the
circuit's GC generation is triggered. One important aspect to understand is that circuits
have a long lifetime, which means that most of the objects rooted by the circuit
eventually reach Gen 2. As a result, you might not see those objects released until a Gen
2 collection happens.

Measure memory usage in general


Prerequisites:

The app must be published in Release configuration. Debug configuration


measurements aren't relevant, as the generated code isn't representative of the
code used for a production deployment.
The app must run without a debugger attached, as this might also affect the
behavior of the app and spoil the results. In Visual Studio, start the app without
debugging by selecting Debug > Start Without Debugging from the menu bar or
Ctrl + F5 using the keyboard.
Consider the different types of memory to understand how much memory is
actually used by .NET. Generally, developers inspect app memory usage in Task
Manager on Windows OS, which typically offers an upper bound of the actual
memory in use. For more information, consult the following articles:
.NET Memory Performance Analysis : In particular, see the section on Memory
Fundamentals .
Work flow of diagnosing memory performance issues (three-part series) : Links
to the three articles of the series are at the top of each article in the series.

Memory usage applied to Blazor


We compute the memory used by blazor as follows:

(Active Circuits × Per-circuit Memory) + (Disconnected Circuits × Per-circuit Memory)

The amount of memory a circuit uses and the maximum potential active circuits that an
app can maintain is largely dependent on how the app is written. The maximum number
of possible active circuits is roughly described by:

Maximum Available Memory / Per-circuit Memory = Maximum Potential Active


Circuits

For a memory leak to occur in Blazor, the following must be true:

The memory must be allocated by the framework, not the app. If you allocate a 1
GB array in the app, the app must manage the disposal of the array.
The memory must not be actively used, which means the circuit isn't active and has
been evicted from the disconnected circuits cache. If you have the maximum active
circuits running, running out of memory is a scale issue, not a memory leak.
A garbage collection (GC) for the circuit's GC generation has run, but the garbage
collector hasn't been able to claim the circuit because another object in the
framework is holding a strong reference to the circuit.

In other cases, there's no memory leak. If the circuit is active (connected or


disconnected), the circuit is still in use.

If a collection for the circuit's GC generation doesn't run, the memory isn't released
because the garbage collector doesn't need to free the memory at that time.

If a collection for a GC generation runs and frees the circuit, you must validate the
memory against the GC stats, not the process, as .NET might decide to keep the virtual
memory active.

If the memory isn't freed, you must find a circuit that isn't either active or disconnected
and that's rooted by another object in the framework. In any other case, the inability to
free memory is an app issue in developer code.

Reduce memory usage


Adopt any of the following strategies to reduce an app's memory usage:

Limit the total amount of memory used by the .NET process. For more information,
see Runtime configuration options for garbage collection.
Reduce the number of disconnected circuits.
Reduce the time a circuit is allowed to be in the disconnected state.
Trigger a garbage collection manually to perform a collection during downtime
periods.
Configure the garbage collection in Workstation mode, which aggressively triggers
garbage collection, instead of Server mode.

Additional actions
Capture a memory dump of the process when memory demands are high and
identify the objects are taking the most memory and where are those objects are
rooted (what holds a reference to them).
.NET in Server mode doesn't release the memory to the OS immediately unless it
must do so. For more information on project file ( .csproj ) settings to control this
behavior, see Runtime configuration options for garbage collection.
Server GC assumes that your app is the only one running on the system and can
use all the system's resources. If the system has 50 GB, the garbage collector seeks
to use the full 50 GB of available memory before it triggers a Gen 2 collection.

For information on disconnected circuit retention configuration, see ASP.NET Core


Blazor SignalR guidance.
Host and deploy ASP.NET Core Blazor
WebAssembly
Article • 06/05/2023

This article explains how to host and deploy Blazor WebAssembly using ASP.NET Core,
Content Delivery Networks (CDN), file servers, and GitHub Pages.

With the Blazor WebAssembly hosting model:

The Blazor app, its dependencies, and the .NET runtime are downloaded to the
browser in parallel.
The app is executed directly on the browser UI thread.

The following deployment strategies are supported:

The Blazor app is served by an ASP.NET Core app. This strategy is covered in the
Hosted deployment with ASP.NET Core section.
The Blazor app is placed on a static hosting web server or service, where .NET isn't
used to serve the Blazor app. This strategy is covered in the Standalone
deployment section, which includes information on hosting a Blazor WebAssembly
app as an IIS sub-app.
An ASP.NET Core app hosts multiple Blazor WebAssembly apps. For more
information, see Multiple hosted ASP.NET Core Blazor WebAssembly apps.

Ahead-of-time (AOT) compilation


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.

Without enabling AOT compilation, Blazor WebAssembly apps run on the browser using
a .NET Intermediate Language (IL) interpreter implemented in WebAssembly. Because
the .NET code is interpreted, apps typically run slower than they would on a server-side
.NET just-in-time (JIT) runtime. AOT compilation addresses this performance issue by
compiling an app's .NET code directly into WebAssembly for native WebAssembly
execution by the browser. The AOT performance improvement can yield dramatic
improvements for apps that execute CPU-intensive tasks. The drawback to using AOT
compilation is that AOT-compiled apps are generally larger than their IL-interpreted
counterparts, so they usually take longer to download to the client when first requested.
For guidance on installing the .NET WebAssembly build tools, see Tooling for ASP.NET
Core Blazor.

To enable WebAssembly AOT compilation, add the <RunAOTCompilation> property set to


true to the Blazor WebAssembly app's project file:

XML

<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

To compile the app to WebAssembly, publish the app. Publishing the Release
configuration ensures the .NET Intermediate Language (IL) linking is also run to reduce
the size of the published app:

.NET CLI

dotnet publish -c Release

WebAssembly AOT compilation is only performed when the project is published. AOT
compilation isn't used when the project is run during development ( Development
environment) because AOT compilation usually takes several minutes on small projects
and potentially much longer for larger projects. Reducing the build time for AOT
compilation is under development for future releases of ASP.NET Core.

The size of an AOT-compiled Blazor WebAssembly app is generally larger than the size
of the app if compiled into .NET IL:

Although the size difference depends on the app, most AOT-compiled apps are
about twice the size of their IL-compiled versions. This means that using AOT
compilation trades off load-time performance for runtime performance. Whether
this tradeoff is worth using AOT compilation depends on your app. Blazor
WebAssembly apps that are CPU intensive generally benefit the most from AOT
compilation.

The larger size of an AOT-compiled app is due to two conditions:


More code is required to represent high-level .NET IL instructions in native
WebAssembly.
AOT does not trim out managed DLLs when the app is published. Blazor
requires the DLLs for reflection metadata and to support certain .NET runtime
features. Requiring the DLLs on the client increases the download size but
provides a more compatible .NET experience.
7 Note

For Mono /WebAssembly MSBuild properties and targets, see WasmApp.targets


(dotnet/runtime GitHub repository) . Official documentation for common
MSBuild properties is planned per Document blazor msbuild configuration
options (dotnet/docs #27395) .

Runtime relinking
One of the largest parts of a Blazor WebAssembly app is the WebAssembly-based .NET
runtime ( dotnet.wasm ) that the browser must download when the app is first accessed
by a user's browser. Relinking the .NET WebAssembly runtime trims unused runtime
code and thus improves download speed.

Runtime relinking requires installation of the .NET WebAssembly build tools. For more
information, see Tooling for ASP.NET Core Blazor.

With the .NET WebAssembly build tools installed, runtime relinking is performed
automatically when an app is published in the Release configuration. The size reduction
is particularly dramatic when disabling globalization. For more information, see ASP.NET
Core Blazor globalization and localization.

) Important

Runtime relinking trims class instance JavaScript-invokable .NET methods unless


they're protected. For more information, see Call .NET methods from JavaScript
functions in ASP.NET Core Blazor.

Customize how boot resources are loaded


Customize how boot resources are loaded using the loadBootResource API. For more
information, see ASP.NET Core Blazor startup.

Compression
When a Blazor WebAssembly app is published, the output is statically compressed
during publish to reduce the app's size and remove the overhead for runtime
compression. The following compression algorithms are used:
Brotli (highest level)
Gzip

Blazor relies on the host to serve the appropriate compressed files. When using an
ASP.NET Core Hosted Blazor WebAssembly project, the host project is capable of
performing content negotiation and serving the statically-compressed files. When
hosting a Blazor WebAssembly standalone app, additional work might be required to
ensure that statically-compressed files are served:

For IIS web.config compression configuration, see the IIS: Brotli and Gzip
compression section.

When hosting on static hosting solutions that don't support statically-compressed


file content negotiation, such as GitHub Pages, consider configuring the app to
fetch and decode Brotli compressed files:

Obtain the JavaScript Brotli decoder from the google/brotli GitHub


repository . The minified decoder file is named decode.min.js and found in
the repository's js folder .

7 Note

If the minified version of the decode.js script ( decode.min.js ) fails, try


using the unminified version ( decode.js ) instead.

Update the app to use the decoder.

In the wwwroot/index.html file, set autostart to false on Blazor's <script> tag:

HTML

<script src="_framework/blazor.webassembly.js" autostart="false">


</script>

After Blazor's <script> tag and before the closing </body> tag, add the
following JavaScript code <script> block:

HTML

<script type="module">
import { BrotliDecode } from './decode.min.js';
Blazor.start({
loadBootResource: function (type, name, defaultUri, integrity) {
if (type !== 'dotnetjs' && location.hostname !== 'localhost')
{
return (async function () {
const response = await fetch(defaultUri + '.br', { cache:
'no-cache' });
if (!response.ok) {
throw new Error(response.statusText);
}
const originalResponseBuffer = await
response.arrayBuffer();
const originalResponseArray = new
Int8Array(originalResponseBuffer);
const decompressedResponseArray =
BrotliDecode(originalResponseArray);
const contentType = type ===
'dotnetwasm' ? 'application/wasm' : 'application/octet-
stream';
return new Response(decompressedResponseArray,
{ headers: { 'content-type': contentType } });
})();
}
}
});
</script>

For more information on loading boot resources, see ASP.NET Core Blazor
startup.

To disable compression, add the BlazorEnableCompression MSBuild property to the


app's project file and set the value to false :

XML

<PropertyGroup>
<BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup>

The BlazorEnableCompression property can be passed to the dotnet publish command


with the following syntax in a command shell:

.NET CLI

dotnet publish -p:BlazorEnableCompression=false

Rewrite URLs for correct routing


Routing requests for page components in a Blazor WebAssembly app isn't as
straightforward as routing requests in a Blazor Server, hosted app. Consider a Blazor
WebAssembly app with two components:

Main.razor : Loads at the root of the app and contains a link to the About
component ( href="About" ).
About.razor : About component.

When the app's default document is requested using the browser's address bar (for
example, https://www.contoso.com/ ):

1. The browser makes a request.


2. The default page is returned, which is usually index.html .
3. index.html bootstraps the app.
4. Blazor's router loads, and the Razor Main component is rendered.

In the Main page, selecting the link to the About component works on the client
because the Blazor router stops the browser from making a request on the Internet to
www.contoso.com for About and serves the rendered About component itself. All of the

requests for internal endpoints within the Blazor WebAssembly app work the same way:
Requests don't trigger browser-based requests to server-hosted resources on the
Internet. The router handles the requests internally.

If a request is made using the browser's address bar for www.contoso.com/About , the
request fails. No such resource exists on the app's Internet host, so a 404 - Not Found
response is returned.

Because browsers make requests to Internet-based hosts for client-side pages, web
servers and hosting services must rewrite all requests for resources not physically on the
server to the index.html page. When index.html is returned, the app's Blazor router
takes over and responds with the correct resource.

When deploying to an IIS server, you can use the URL Rewrite Module with the app's
published web.config file. For more information, see the IIS section.

Hosted deployment with ASP.NET Core


A hosted deployment serves the Blazor WebAssembly app to browsers from an ASP.NET
Core app that runs on a web server.

The client Blazor WebAssembly app is published into the /bin/Release/{TARGET


FRAMEWORK}/publish/wwwroot folder of the server app, along with any other static web

assets of the server app. The two apps are deployed together. A web server that is
capable of hosting an ASP.NET Core app is required. For a hosted deployment, Visual
Studio includes the Blazor WebAssembly App project template ( blazorwasm template
when using the dotnet new command) with the Hosted option selected ( -ho|--hosted
when using the dotnet new command).

For more information, see the following articles:

ASP.NET Core app hosting and deployment: Host and deploy ASP.NET Core
Deployment to Azure App Service: Publish an ASP.NET Core app to Azure with
Visual Studio
Blazor project templates: ASP.NET Core Blazor project structure

Hosted deployment of a framework-dependent


executable for a specific platform
To deploy a hosted Blazor WebAssembly app as a framework-dependent executable for
a specific platform (not self-contained) use the following guidance based on the tooling
in use.

Visual Studio
By default, a self-contained deployment is configured for a generated publish profile
( .pubxml ). Confirm that the Server project's publish profile contains the
<SelfContained> MSBuild property set to false .

In the .pubxml publish profile file in the Server project's Properties folder:

XML

<SelfContained>false</SelfContained>

Set the Runtime Identifier (RID) using the Target Runtime setting in the Settings area of
the Publish UI, which generates the <RuntimeIdentifier> MSBuild property in the
publish profile:

XML

<RuntimeIdentifier>{RID}</RuntimeIdentifier>

In the preceding configuration, the {RID} placeholder is the Runtime Identifier (RID).

Publish the Server project in the Release configuration.


7 Note

It's possible to publish an app with publish profile settings using the .NET CLI by
passing /p:PublishProfile={PROFILE} to the dotnet publish command, where the
{PROFILE} placeholder is the profile. For more information, see the Publish profiles

and Folder publish example sections in the Visual Studio publish profiles (.pubxml)
for ASP.NET Core app deployment article. If you pass the RID in the dotnet publish
command and not in the publish profile, use the MSBuild property
( /p:RuntimeIdentifier ) with the command, not with the -r|--runtime option.

.NET CLI
Configure a self-contained deployment by placing the <SelfContained> MSBuild
property in a <PropertyGroup> in the Server project's project file set to false :

XML

<SelfContained>false</SelfContained>

) Important

The SelfContained property must be placed in the Server project's project file. The
property can't be set correctly with the dotnet publish command using the --no-
self-contained option or the MSBuild property /p:SelfContained=false .

Set the Runtime Identifier (RID) using either of the following approaches:

Option 1: Set the RID in a <PropertyGroup> in the Server project's project file:

XML

<RuntimeIdentifier>{RID}</RuntimeIdentifier>

In the preceding configuration, the {RID} placeholder is the Runtime Identifier


(RID).

Publish the app in the Release configuration from the Server project:

.NET CLI
dotnet publish -c Release

Option 2: Pass the RID in the dotnet publish command as the MSBuild property
( /p:RuntimeIdentifier ), not with the -r|--runtime option:

.NET CLI

dotnet publish -c Release /p:RuntimeIdentifier={RID}

In the preceding command, the {RID} placeholder is the Runtime Identifier (RID).

For more information, see the following articles:

.NET application publishing overview


Host and deploy ASP.NET Core

Hosted deployment with multiple Blazor


WebAssembly apps
For more information, see Multiple hosted ASP.NET Core Blazor WebAssembly apps.

Standalone deployment
A standalone deployment serves the Blazor WebAssembly app as a set of static files that
are requested directly by clients. Any static file server is able to serve the Blazor app.

Standalone deployment assets are published into the /bin/Release/{TARGET


FRAMEWORK}/publish/wwwroot folder.

Azure App Service


Blazor WebAssembly apps can be deployed to Azure App Services on Windows, which
hosts the app on IIS.

Deploying a standalone Blazor WebAssembly app to Azure App Service for Linux isn't
currently supported. We recommend hosting a standalone Blazor WebAssembly app
using Azure Static Web Apps, which supports this scenario.

Azure Static Web Apps


Deploy a Blazor WebAssembly app to Azure Static Web Apps using either of the
following approaches:

Deploy from Visual Studio


Deploy from GitHub

Deploy from Visual Studio

To deploy from Visual Studio, create a publish profile for Azure Static Web Apps:

1. Save any unsaved work in the project, as a Visual Studio restart might be required
during the process.

2. In Visual Studio's Publish UI, select Target > Azure > Specific Target > Azure
Static Web Apps to create a publish profile.

3. If the Azure WebJobs Tools component for Visual Studio isn't installed, a prompt
appears to install the ASP.NET and web development component. Follow the
prompts to install the tools using the Visual Studio Installer. Visual Studio closes
and re-opens automatically while installing the tools. After the tools are installed,
start over at the first step to create the publish profile.

4. In the publish profile configuration, provide the Subscription name. Select an


existing instance, or select Create a new instance. When creating a new instance in
the Azure portal's Create Static Web App UI, set the Deployment details > Source
to Other. Wait for the deployment to complete in the Azure portal before
proceeding.

5. In the publish profile configuration, select the Azure Static Web Apps instance from
the instance's resource group. Select Finish to create the publish profile. If Visual
Studio prompts to install the Static Web Apps (SWA) CLI, install the CLI by
following the prompts. The SWA CLI requires NPM/Node.js (Visual Studio
documentation).

After the publish profile is created, deploy the app to the Azure Static Web Apps
instance using the publish profile by selecting the Publish button.

Deploy from GitHub


To deploy from a GitHub repository, see Tutorial: Building a static web app with Blazor in
Azure Static Web Apps.

IIS
IIS is a capable static file server for Blazor apps. To configure IIS to host Blazor, see Build
a Static Website on IIS.

Published assets are created in the /bin/Release/{TARGET FRAMEWORK}/publish or


bin\Release\{TARGET FRAMEWORK}\browser-wasm\publish folder, depending on which
version of the SDK is used and where the {TARGET FRAMEWORK} placeholder is the target
framework. Host the contents of the publish folder on the web server or hosting
service.

web.config

When a Blazor project is published, a web.config file is created with the following IIS
configuration:

MIME types
HTTP compression is enabled for the following MIME types:
application/octet-stream

application/wasm
URL Rewrite Module rules are established:
Serve the sub-directory where the app's static assets reside ( wwwroot/{PATH
REQUESTED} ).

Create SPA fallback routing so that requests for non-file assets are redirected to
the app's default document in its static assets folder ( wwwroot/index.html ).

Use a custom web.config

To use a custom web.config file:

1. Place the custom web.config file in the project's root folder. For a hosted Blazor
WebAssembly solution, place the file in the Server project's folder.
2. Publish the project. For a hosted Blazor WebAssembly solution, publish the
solution from the Server project. For more information, see Host and deploy
ASP.NET Core Blazor.

If the SDK's web.config generation or transformation during publish either doesn't


move the file to published assets in the publish folder or modifies the custom
configuration in your custom web.config file, use any of the following approaches as
needed to take full control of the process:

If the SDK doesn't generate the file, for example, in a standalone Blazor
WebAssembly app at /bin/Release/{TARGET FRAMEWORK}/publish/wwwroot or
bin\Release\{TARGET FRAMEWORK}\browser-wasm\publish , depending on which

version of the SDK is used and where the {TARGET FRAMEWORK} placeholder is the
target framework, set the <PublishIISAssets> property to true in the project file
( .csproj ). Usually for standalone WebAssembly apps, this is the only required
setting to move a custom web.config file and prevent transformation of the file by
the SDK.

XML

<PropertyGroup>
<PublishIISAssets>true</PublishIISAssets>
</PropertyGroup>

Disable the SDK's web.config transformation in the project file ( .csproj ):

XML

<PropertyGroup>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>

Add a custom target to the project file ( .csproj ) to move a custom web.config
file. In the following example, the custom web.config file is placed by the
developer at the root of the project. If the web.config file resides elsewhere,
specify the path to the file in SourceFiles . The following example specifies the
publish folder with $(PublishDir) , but provide a path to DestinationFolder for a

custom output location.

XML

<Target Name="CopyWebConfig" AfterTargets="Publish">


<Copy SourceFiles="web.config" DestinationFolder="$(PublishDir)" />
</Target>

Install the URL Rewrite Module


The URL Rewrite Module is required to rewrite URLs. The module isn't installed by
default, and it isn't available for install as a Web Server (IIS) role service feature. The
module must be downloaded from the IIS website. Use the Web Platform Installer to
install the module:
1. Locally, navigate to the URL Rewrite Module downloads page . For the English
version, select WebPI to download the WebPI installer. For other languages, select
the appropriate architecture for the server (x86/x64) to download the installer.
2. Copy the installer to the server. Run the installer. Select the Install button and
accept the license terms. A server restart isn't required after the install completes.

Configure the website


Set the website's Physical path to the app's folder. The folder contains:

The web.config file that IIS uses to configure the website, including the required
redirect rules and file content types.
The app's static asset folder.

Host as an IIS sub-app


If a standalone app is hosted as an IIS sub-app, perform either of the following:

Disable the inherited ASP.NET Core Module handler.

Remove the handler in the Blazor app's published web.config file by adding a
<handlers> section to the <system.webServer> section of the file:

XML

<handlers>
<remove name="aspNetCore" />
</handlers>

Disable inheritance of the root (parent) app's <system.webServer> section using a


<location> element with inheritInChildApplications set to false :

XML

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


<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" ... />
</handlers>
<aspNetCore ... />
</system.webServer>
</location>
</configuration>
7 Note

Disabling inheritance of the root (parent) app's <system.webServer> section is


the default configuration for published apps using the .NET SDK.

Removing the handler or disabling inheritance is performed in addition to configuring


the app's base path. Set the app base path in the app's index.html file to the IIS alias
used when configuring the sub-app in IIS.

Brotli and Gzip compression


This section only applies to standalone Blazor WebAssembly apps. Hosted Blazor apps use
a default ASP.NET Core app web.config file, not the file linked in this section.

IIS can be configured via web.config to serve Brotli or Gzip compressed Blazor assets for
standalone Blazor WebAssembly apps. For an example configuration file, see
web.config .

Additional configuration of the example web.config file might be required in the


following scenarios:

The app's specification calls for either of the following:


Serving compressed files that aren't configured by the example web.config file.
Serving compressed files configured by the example web.config file in an
uncompressed format.
The server's IIS configuration (for example, applicationHost.config ) provides
server-level IIS defaults. Depending on the server-level configuration, the app
might require a different IIS configuration than what the example web.config file
contains.

For more information on custom web.config files, see the Use a custom web.config
section.

Troubleshooting

If a 500 - Internal Server Error is received and IIS Manager throws errors when
attempting to access the website's configuration, confirm that the URL Rewrite Module
is installed. When the module isn't installed, the web.config file can't be parsed by IIS.
This prevents the IIS Manager from loading the website's configuration and the website
from serving Blazor's static files.
For more information on troubleshooting deployments to IIS, see Troubleshoot ASP.NET
Core on Azure App Service and IIS.

Azure Storage
Azure Storage static file hosting allows serverless Blazor app hosting. Custom domain
names, the Azure Content Delivery Network (CDN), and HTTPS are supported.

When the blob service is enabled for static website hosting on a storage account:

Set the Index document name to index.html .


Set the Error document path to index.html . Razor components and other non-file
endpoints don't reside at physical paths in the static content stored by the blob
service. When a request for one of these resources is received that the Blazor
router should handle, the 404 - Not Found error generated by the blob service
routes the request to the Error document path. The index.html blob is returned,
and the Blazor router loads and processes the path.

If files aren't loaded at runtime due to inappropriate MIME types in the files' Content-
Type headers, take either of the following actions:

Configure your tooling to set the correct MIME types ( Content-Type headers) when
the files are deployed.

Change the MIME types ( Content-Type headers) for the files after the app is
deployed.

In Storage Explorer (Azure portal) for each file:

1. Right-click the file and select Properties.


2. Set the ContentType and select the Save button.

For more information, see Static website hosting in Azure Storage.

Nginx
The following nginx.conf file is simplified to show how to configure Nginx to send the
index.html file whenever it can't find a corresponding file on disk.

events { }
http {
server {
listen 80;

location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html =404;
}
}
}

When setting the NGINX burst rate limit with limit_req , Blazor WebAssembly apps
may require a large burst parameter value to accommodate the relatively large number
of requests made by an app. Initially, set the value to at least 60:

http {
server {
...

location / {
...

limit_req zone=one burst=60 nodelay;


}
}
}

Increase the value if browser developer tools or a network traffic tool indicates that
requests are receiving a 503 - Service Unavailable status code.

For more information on production Nginx web server configuration, see Creating
NGINX Plus and NGINX Configuration Files .

Apache
To deploy a Blazor WebAssembly app to CentOS 7 or later:

1. Create the Apache configuration file. The following example is a simplified


configuration file ( blazorapp.config ):

<VirtualHost *:80>
ServerName www.example.com
ServerAlias *.example.com

DocumentRoot "/var/www/blazorapp"
ErrorDocument 404 /index.html
AddType application/wasm .wasm
AddType application/octet-stream .dll

<Directory "/var/www/blazorapp">
Options -Indexes
AllowOverride None
</Directory>

<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE application/octet-stream
AddOutputFilterByType DEFLATE application/wasm
<IfModule mod_setenvif.c>
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4.0[678] no-gzip
BrowserMatch bMSIE !no-gzip !gzip-only-text/html
</IfModule>
</IfModule>

ErrorLog /var/log/httpd/blazorapp-error.log
CustomLog /var/log/httpd/blazorapp-access.log common
</VirtualHost>

2. Place the Apache configuration file into the /etc/httpd/conf.d/ directory, which is
the default Apache configuration directory in CentOS 7.

3. Place the app's files into the /var/www/blazorapp directory (the location specified
to DocumentRoot in the configuration file).

4. Restart the Apache service.

For more information, see mod_mime and mod_deflate .

GitHub Pages
The default GitHub Action, which deploys pages, skips deployment of folders starting
with underscore, for example, the _framework folder. To deploy folders starting with
underscore, add an empty .nojekyll file to the Git branch.

Git treats JavaScript (JS) files, such as blazor.webassembly.js , as text and converts line
endings from CRLF (carriage return-line feed) to LF (line feed) in the deployment
pipeline. These changes to JS files produce different file hashes than Blazor sends to the
client in the blazor.boot.json file. The mismatches result in integrity check failures on
the client. One approach to solving this problem is to add a .gitattributes file with
*.js binary line before adding the app's assets to the Git branch. The *.js binary line

configures Git to treat JS files as binary files, which avoids processing the files in the
deployment pipeline. The file hashes of the unprocessed files match the entries in the
blazor.boot.json file, and client-side integrity checks pass. For more information, see
the Resolve integrity check failures section.

To handle URL rewrites, add a wwwroot/404.html file with a script that handles
redirecting the request to the index.html page. For an example, see the
SteveSandersonMS/BlazorOnGitHubPages GitHub repository :

wwwroot/404.html
Live site

When using a project site instead of an organization site, update the <base> tag in
wwwroot/index.html . Set the href attribute value to the GitHub repository name with a
trailing slash (for example, /my-repository/ ). In the
SteveSandersonMS/BlazorOnGitHubPages GitHub repository , the base href is
updated at publish by the .github/workflows/main.yml configuration file .

7 Note

The SteveSandersonMS/BlazorOnGitHubPages GitHub repository isn't owned,


maintained, or supported by the .NET Foundation or Microsoft.

Standalone with Docker


A standalone Blazor WebAssembly app is published as a set of static files for hosting by
a static file server.

To host the app in Docker:

Choose a Docker container with web server support, such as Ngnix or Apache.
Copy the publish folder assets to a location folder defined in the web server for
serving static files.
Apply additional configuration as needed to serve the Blazor WebAssembly app.

For configuration guidance, see the following resources:

Nginx section or Apache section of this article


Docker Documentation
Host configuration values
Blazor WebAssembly apps can accept the following host configuration values as
command-line arguments at runtime in the development environment.

Content root
The --contentroot argument sets the absolute path to the directory that contains the
app's content files (content root). In the following examples, /content-root-path is the
app's content root path.

Pass the argument when running the app locally at a command prompt. From the
app's directory, execute:

.NET CLI

dotnet run --contentroot=/content-root-path

Add an entry to the app's launchSettings.json file in the IIS Express profile. This
setting is used when the app is run with the Visual Studio Debugger and from a
command prompt with dotnet run .

JSON

"commandLineArgs": "--contentroot=/content-root-path"

In Visual Studio, specify the argument in Properties > Debug > Application
arguments. Setting the argument in the Visual Studio property page adds the
argument to the launchSettings.json file.

Console

--contentroot=/content-root-path

Path base
The --pathbase argument sets the app base path for an app run locally with a non-root
relative URL path (the <base> tag href is set to a path other than / for staging and
production). In the following examples, /relative-URL-path is the app's path base. For
more information, see App base path.
) Important

Unlike the path provided to href of the <base> tag, don't include a trailing slash
( / ) when passing the --pathbase argument value. If the app base path is provided
in the <base> tag as <base href="/CoolApp/"> (includes a trailing slash), pass the
command-line argument value as --pathbase=/CoolApp (no trailing slash).

Pass the argument when running the app locally at a command prompt. From the
app's directory, execute:

.NET CLI

dotnet run --pathbase=/relative-URL-path

Add an entry to the app's launchSettings.json file in the IIS Express profile. This
setting is used when running the app with the Visual Studio Debugger and from a
command prompt with dotnet run .

JSON

"commandLineArgs": "--pathbase=/relative-URL-path"

In Visual Studio, specify the argument in Properties > Debug > Application
arguments. Setting the argument in the Visual Studio property page adds the
argument to the launchSettings.json file.

Console

--pathbase=/relative-URL-path

URLs
The --urls argument sets the IP addresses or host addresses with ports and protocols
to listen on for requests.

Pass the argument when running the app locally at a command prompt. From the
app's directory, execute:

.NET CLI

dotnet run --urls=http://127.0.0.1:0


Add an entry to the app's launchSettings.json file in the IIS Express profile. This
setting is used when running the app with the Visual Studio Debugger and from a
command prompt with dotnet run .

JSON

"commandLineArgs": "--urls=http://127.0.0.1:0"

In Visual Studio, specify the argument in Properties > Debug > Application
arguments. Setting the argument in the Visual Studio property page adds the
argument to the launchSettings.json file.

Console

--urls=http://127.0.0.1:0

Hosted deployment on Linux (Nginx)


Configure the app with ForwardedHeadersOptions to forward the X-Forwarded-For and
X-Forwarded-Proto headers by following the guidance in Configure ASP.NET Core to
work with proxy servers and load balancers.

For more information on setting the app's base path, including sub-app path
configuration, see Host and deploy ASP.NET Core Blazor.

Follow the guidance for an ASP.NET Core SignalR app with the following changes:

Remove the configuration for proxy buffering ( proxy_buffering off; ) because the
setting only applies to Server-Sent Events (SSE) , which aren't relevant to Blazor
app client-server interactions.

Change the location path from /hubroute ( location /hubroute { ... } ) to the
sub-app path /{PATH} ( location /{PATH} { ... } ), where the {PATH} placeholder
is the sub-app path.

The following example configures the server for an app that responds to requests
at the root path / :

http {
server {
...
location / {
...
}
}
}

The following example configures the sub-app path of /blazor :

http {
server {
...
location /blazor {
...
}
}
}

For more information and configuration guidance, consult the following resources:

Host ASP.NET Core on Linux with Nginx


Nginx documentation:
NGINX as a WebSocket Proxy
WebSocket proxying
Developers on non-Microsoft support forums:
Stack Overflow (tag: blazor)
ASP.NET Core Slack Team
Blazor Gitter

Configure the Trimmer


Blazor performs Intermediate Language (IL) trimming on each Release build to remove
unnecessary IL from the output assemblies. For more information, see Configure the
Trimmer for ASP.NET Core Blazor.

Change the file name extension of DLL files


If a firewall, anti-virus program, or network security appliance is blocking the
transmission of the app's dynamic-link library (DLL) files ( .dll ), you can follow the
guidance in this section to change the file name extensions of the app's published DLL
files.
7 Note

Changing the file name extensions of the app's DLL files might not resolve the
problem because many security systems scan the content of the app's files, not
merely check file extensions. For a more robust approach in environments that
block the download and execution of DLL files, see Deployment layout for ASP.NET
Core Blazor WebAssembly apps.

Third-party approaches exist for dealing with this problem. For more information,
see the resources at Awesome Blazor .

After publishing the app, use a shell script or DevOps build pipeline to rename .dll files
to use a different file extension in the directory of the app's published output.

In the following examples:

PowerShell (PS) is used to update the file extensions.


.dll files are renamed to use the .bin file extension from the command line.

Files listed in the published blazor.boot.json file with a .dll file extension are
updated to the .bin file extension.
If service worker assets are also in use, a PowerShell command updates the .dll
files listed in the service-worker-assets.js file to the .bin file extension.

To use a different file extension than .bin , replace .bin in the following commands
with the desired file extension.

On Windows:

PowerShell

dir {PATH} | rename-item -NewName { $_.name -replace ".dll\b",".bin" }


((Get-Content {PATH}\blazor.boot.json -Raw) -replace '.dll"','.bin"') | Set-
Content {PATH}\blazor.boot.json

In the preceding command, the {PATH} placeholder is the path to the published
_framework folder (for example, .\bin\Release\net6.0\browser-

wasm\publish\wwwroot\_framework from the project's root folder).

If service worker assets are also in use:

PowerShell
((Get-Content {PATH}\service-worker-assets.js -Raw) -replace
'.dll"','.bin"') | Set-Content {PATH}\service-worker-assets.js

In the preceding command, the {PATH} placeholder is the path to the published
service-worker-assets.js file.

On Linux or macOS:

Console

for f in {PATH}/*; do mv "$f" "`echo $f | sed -e 's/\.dll/.bin/g'`"; done


sed -i 's/\.dll"/.bin"/g' {PATH}/blazor.boot.json

In the preceding command, the {PATH} placeholder is the path to the published
_framework folder (for example, .\bin\Release\net6.0\browser-

wasm\publish\wwwroot\_framework from the project's root folder).

If service worker assets are also in use:

Console

sed -i 's/\.dll"/.bin"/g' {PATH}/service-worker-assets.js

In the preceding command, the {PATH} placeholder is the path to the published
service-worker-assets.js file.

To address the compressed blazor.boot.json.gz and blazor.boot.json.br files, adopt


either of the following approaches:

Remove the compressed blazor.boot.json.gz and blazor.boot.json.br files.


Compression is disabled with this approach.
Recompress the updated blazor.boot.json file.

The preceding guidance for the compressed blazor.boot.json file also applies when
service worker assets are in use. Remove or recompress service-worker-assets.js.br
and service-worker-assets.js.gz . Otherwise, file integrity checks fail in the browser.

The following Windows example for .NET 6.0 uses a PowerShell script placed at the root
of the project. The following script, which disables compression, is the basis for further
modification if you wish to recompress the blazor.boot.json file.

ChangeDLLExtensions.ps1: :
PowerShell

param([string]$filepath,[string]$tfm)
dir $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework |
rename-item -NewName { $_.name -replace ".dll\b",".bin" }
((Get-Content $filepath\bin\Release\$tfm\browser-
wasm\publish\wwwroot\_framework\blazor.boot.json -Raw) -replace
'.dll"','.bin"') | Set-Content $filepath\bin\Release\$tfm\browser-
wasm\publish\wwwroot\_framework\blazor.boot.json
Remove-Item $filepath\bin\Release\$tfm\browser-
wasm\publish\wwwroot\_framework\blazor.boot.json.gz
Remove-Item $filepath\bin\Release\$tfm\browser-
wasm\publish\wwwroot\_framework\blazor.boot.json.br

If service worker assets are also in use, add the following commands:

PowerShell

((Get-Content $filepath\bin\Release\$tfm\browser-
wasm\publish\wwwroot\service-worker-assets.js -Raw) -replace
'.dll"','.bin"') | Set-Content $filepath\bin\Release\$tfm\browser-
wasm\publish\wwwroot\_framework\wwwroot\service-worker-assets.js
Remove-Item $filepath\bin\Release\$tfm\browser-
wasm\publish\wwwroot\_framework\wwwroot\service-worker-assets.js.gz
Remove-Item $filepath\bin\Release\$tfm\browser-
wasm\publish\wwwroot\_framework\wwwroot\service-worker-assets.js.br

In the project file, the script is executed after publishing the app for the Release
configuration:

XML

<Target Name="ChangeDLLFileExtensions" AfterTargets="AfterPublish"


Condition="'$(Configuration)'=='Release'">
<Exec Command="powershell.exe -command &quot;&amp; {
.\ChangeDLLExtensions.ps1 '$(SolutionDir)' '$(TargetFramework)'}&quot;" />
</Target>

7 Note

When renaming and lazy loading the same assemblies, see the guidance in Lazy
load assemblies in ASP.NET Core Blazor WebAssembly.

Usually, the app's server requires static asset configuration to serve the files with the
updated extension. For an app hosted by IIS, add a MIME map entry ( <mimeMap> ) for the
new file extension in the static content section ( <staticContent> ) in a custom
web.config file. The following example assumes that the file extension is changed from

.dll to .bin :

XML

<staticContent>
...
<mimeMap fileExtension=".bin" mimeType="application/octet-stream" />
...
</staticContent>

Include an update for compressed files if compression is in use:

<mimeMap fileExtension=".bin.br" mimeType="application/octet-stream" />


<mimeMap fileExtension=".bin.gz" mimeType="application/octet-stream" />

Remove the entry for the .dll file extension:

diff

- <mimeMap fileExtension=".dll" mimeType="application/octet-stream" />

Remove entries for compressed .dll files if compression is in use:

diff

- <mimeMap fileExtension=".dll.br" mimeType="application/octet-stream" />


- <mimeMap fileExtension=".dll.gz" mimeType="application/octet-stream" />

For more information on custom web.config files, see the Use a custom web.config
section.

Prior deployment corruption


Typically on deployment:

Only the files that have changed are replaced, which usually results in a faster
deployment.
Existing files that aren't part of the new deployment are left in place for use by the
new deployment.
In rare cases, lingering files from a prior deployment can corrupt a new deployment.
Completely deleting the existing deployment (or locally-published app prior to
deployment) may resolve the issue with a corrupted deployment. Often, deleting the
existing deployment once is sufficient to resolve the problem, including for a DevOps
build and deploy pipeline.

If you determine that clearing a prior deployment is always required when a DevOps
build and deploy pipeline is in use, you can temporarily add a step to the build pipeline
to delete the prior deployment for each new deployment until you troubleshoot the
exact cause of the corruption.

Resolve integrity check failures


When Blazor WebAssembly downloads an app's startup files, it instructs the browser to
perform integrity checks on the responses. Blazor sends SHA-256 hash values for DLL
( .dll ), WebAssembly ( .wasm ), and other files in the blazor.boot.json file, which isn't
cached on clients. The file hashes of cached files are compared to the hashes in the
blazor.boot.json file. For cached files with a matching hash, Blazor uses the cached

files. Otherwise, files are requested from the server. After a file is downloaded, its hash is
checked again for integrity validation. An error is generated by the browser if any
downloaded file's integrity check fails.

Blazor's algorithm for managing file integrity:

Ensures that the app doesn't risk loading an inconsistent set of files, for example if
a new deployment is applied to your web server while the user is in the process of
downloading the application files. Inconsistent files can result in a malfunctioning
app.
Ensures the user's browser never caches inconsistent or invalid responses, which
can prevent the app from starting even if the user manually refreshes the page.
Makes it safe to cache the responses and not check for server-side changes until
the expected SHA-256 hashes themselves change, so subsequent page loads
involve fewer requests and complete faster.

If the web server returns responses that don't match the expected SHA-256 hashes, an
error similar to the following example appears in the browser's developer console:

Failed to find a valid digest in the 'integrity' attribute for resource


'https://myapp.example.com/_framework/MyBlazorApp.dll' with computed SHA-256
integrity 'IIa70iwvmEg5WiDV17OpQ5eCztNYqL186J56852RpJY='. The resource has
been blocked.
In most cases, the warning doesn't indicate a problem with integrity checking. Instead,
the warning usually means that some other problem exists.

For Blazor WebAssembly's boot reference source, see the Boot.WebAssembly.ts file in
the dotnet/aspnetcore GitHub repository .

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Diagnosing integrity problems


When an app is built, the generated blazor.boot.json manifest describes the SHA-256
hashes of boot resources at the time that the build output is produced. The integrity
check passes as long as the SHA-256 hashes in blazor.boot.json match the files
delivered to the browser.

Common reasons why this fails include:

The web server's response is an error (for example, a 404 - Not Found or a 500 -
Internal Server Error) instead of the file the browser requested. This is reported by
the browser as an integrity check failure and not as a response failure.
Something has changed the contents of the files between the build and delivery of
the files to the browser. This might happen:
If you or build tools manually modify the build output.
If some aspect of the deployment process modified the files. For example if you
use a Git-based deployment mechanism, bear in mind that Git transparently
converts Windows-style line endings to Unix-style line endings if you commit
files on Windows and check them out on Linux. Changing file line endings
change the SHA-256 hashes. To avoid this problem, consider using .gitattributes
to treat build artifacts as binary files .
The web server modifies the file contents as part of serving them. For example,
some content distribution networks (CDNs) automatically attempt to minify
HTML, thereby modifying it. You may need to disable such features.
The blazor.boot.json file fails to load properly or is improperly cached on the
client. Common causes include either of the following:
Misconfigured or malfunctioning custom developer code.
One or more misconfigured intermediate caching layers.

To diagnose which of these applies in your case:

1. Note which file is triggering the error by reading the error message.
2. Open your browser's developer tools and look in the Network tab. If necessary,
reload the page to see the list of requests and responses. Find the file that is
triggering the error in that list.
3. Check the HTTP status code in the response. If the server returns anything other
than 200 - OK (or another 2xx status code), then you have a server-side problem to
diagnose. For example, status code 403 means there's an authorization problem,
whereas status code 500 means the server is failing in an unspecified manner.
Consult server-side logs to diagnose and fix the app.
4. If the status code is 200 - OK for the resource, look at the response content in
browser's developer tools and check that the content matches up with the data
expected. For example, a common problem is to misconfigure routing so that
requests return your index.html data even for other files. Make sure that
responses to .wasm requests are WebAssembly binaries and that responses to
.dll requests are .NET assembly binaries. If not, you have a server-side routing
problem to diagnose.
5. Seek to validate the app's published and deployed output with the Troubleshoot
integrity PowerShell script.

If you confirm that the server is returning plausibly correct data, there must be
something else modifying the contents in between build and delivery of the file. To
investigate this:

Examine the build toolchain and deployment mechanism in case they're modifying
files after the files are built. An example of this is when Git transforms file line
endings, as described earlier.
Examine the web server or CDN configuration in case they're set up to modify
responses dynamically (for example, trying to minify HTML). It's fine for the web
server to implement HTTP compression (for example, returning content-encoding:
br or content-encoding: gzip ), since this doesn't affect the result after

decompression. However, it's not fine for the web server to modify the
uncompressed data.

Troubleshoot integrity PowerShell script


Use the integrity.ps1 PowerShell script to validate a published and deployed Blazor
app. The script is provided for PowerShell Core 7 or later as a starting point when the
app has integrity issues that the Blazor framework can't identify. Customization of the
script might be required for your apps, including if running on version of PowerShell
later than version 7.2.0.

The script checks the files in the publish folder and downloaded from the deployed app
to detect issues in the different manifests that contain integrity hashes. These checks
should detect the most common problems:

You modified a file in the published output without realizing it.


The app wasn't correctly deployed to the deployment target, or something
changed within the deployment target's environment.
There are differences between the deployed app and the output from publishing
the app.

Invoke the script with the following command in a PowerShell command shell:

PowerShell

.\integrity.ps1 {BASE URL} {PUBLISH OUTPUT FOLDER}

In the following example, the script is executed on a locally-running app at


https://localhost:5001/ :

PowerShell

.\integrity.ps1 https://localhost:5001/
C:\TestApps\BlazorSample\bin\Release\net6.0\publish\

Placeholders:

{BASE URL} : The URL of the deployed app. A trailing slash ( / ) is required.

{PUBLISH OUTPUT FOLDER} : The path to the app's publish folder or location where
the app is published for deployment.

7 Note

When cloning the dotnet/AspNetCore.Docs GitHub repository, the integrity.ps1


script might be quarantined by Bitdefender or another virus scanner present on
the system. Usually, the file is trapped by a virus scanner's heuristic scanning
technology, which merely looks for patterns in files that might indicate the
presence of malware. To prevent the virus scanner from quarantining the file, add
an exception to the virus scanner prior to cloning the repo. The following example
is a typical path to the script on a Windows system. Adjust the path as needed for
other systems. The placeholder {USER} is the user's path segment.
C:\Users\
{USER}\Documents\GitHub\AspNetCore.Docs\aspnetcore\blazor\host-and-
deploy\webassembly\_samples\integrity.ps1

Warning: Creating virus scanner exceptions is dangerous and should only be


performed when you're certain that the file is safe.

Comparing the checksum of a file to a valid checksum value doesn't guarantee file
safety, but modifying a file in a way that maintains a checksum value isn't trivial for
malicious users. Therefore, checksums are useful as a general security approach.
Compare the checksum of the local integrity.ps1 file to one of the following
values:

SHA256: 32c24cb667d79a701135cb72f6bae490d81703323f61b8af2c7e5e5dc0f0c2bb
MD5: 9cee7d7ec86ee809a329b5406fbf21a8

Obtain the file's checksum on Windows OS with the following command. Provide
the path and file name for the {PATH AND FILE NAME} placeholder and indicate the
type of checksum to produce for the {SHA512|MD5} placeholder, either SHA256 or
MD5 :

Console

CertUtil -hashfile {PATH AND FILE NAME} {SHA256|MD5}

If you have any cause for concern that checksum validation isn't secure enough in
your environment, consult your organization's security leadership for guidance.

For more information, see Understanding malware & other threats.

Disable integrity checking for non-PWA apps


In most cases, don't disable integrity checking. Disabling integrity checking doesn't
solve the underlying problem that has caused the unexpected responses and results in
losing the benefits listed earlier.

There may be cases where the web server can't be relied upon to return consistent
responses, and you have no choice but to temporarily disable integrity checks until the
underlying problem is resolved.
To disable integrity checks, add the following to a property group in the Blazor
WebAssembly app's project file ( .csproj ):

XML

<BlazorCacheBootResources>false</BlazorCacheBootResources>

BlazorCacheBootResources also disables Blazor's default behavior of caching the .dll ,

.wasm , and other files based on their SHA-256 hashes because the property indicates
that the SHA-256 hashes can't be relied upon for correctness. Even with this setting, the
browser's normal HTTP cache may still cache those files, but whether or not this
happens depends on your web server configuration and the cache-control headers that
it serves.

7 Note

The BlazorCacheBootResources property doesn't disable integrity checks for


Progressive Web Applications (PWAs). For guidance pertaining to PWAs, see the
Disable integrity checking for PWAs section.

We can't provide an exhaustive list of scenarios where disabling integrity checking is


required. Servers can answer a request in arbitrary ways outside of the scope of the
Blazor framework. The framework provides the BlazorCacheBootResources setting to
make the app runnable at the cost of losing a guarantee of integrity that the app can
provide. Again, we don't recommend disabling integrity checking, especially for
production deployments. Developers should seek to solve the underlying integrity
problem that's causing integrity checking to fail.

A few general cases that can cause integrity issues are:

Running on HTTP where integrity can't be checked.


If your deployment process modifies the files after publish in any way.
If your host modifies the files in any way.

Disable integrity checking for PWAs


Blazor's Progressive Web Application (PWA) template contains a suggested service-
worker.published.js file that's responsible for fetching and storing application files for

offline use. This is a separate process from the normal app startup mechanism and has
its own separate integrity checking logic.
Inside the service-worker.published.js file, following line is present:

JavaScript

.map(asset => new Request(asset.url, { integrity: asset.hash }));

To disable integrity checking, remove the integrity parameter by changing the line to
the following:

JavaScript

.map(asset => new Request(asset.url));

Again, disabling integrity checking means that you lose the safety guarantees offered by
integrity checking. For example, there is a risk that if the user's browser is caching the
app at the exact moment that you deploy a new version, it could cache some files from
the old deployment and some from the new deployment. If that happens, the app
becomes stuck in a broken state until you deploy a further update.

SignalR configuration
SignalR's hosting and scaling conditions apply to Blazor apps that use SignalR.

Transports
Blazor works best when using WebSockets as the SignalR transport due to lower latency,
better reliability, and improved security. Long Polling is used by SignalR when
WebSockets isn't available or when the app is explicitly configured to use Long Polling.
When deploying to Azure App Service, configure the app to use WebSockets in the
Azure portal settings for the service. For details on configuring the app for Azure App
Service, see the SignalR publishing guidelines.

A console warning appears if Long Polling is utilized:

Failed to connect via WebSockets, using the Long Polling fallback transport. This
may be due to a VPN or proxy blocking the connection.

Global deployment and connection failures


Recommendations for global deployments to geographical data centers:

Deploy the app to the regions where most of the users reside.
Take into consideration the increased latency for traffic across continents.
For Azure hosting, use the Azure SignalR Service.

If a deployed app frequently displays the reconnection UI due to ping timeouts caused
by Internet latency, lengthen the server and client timeouts:

Server

At least double the maximum roundtrip time expected between the client and the
server. Test, monitor, and revise the timeouts as needed. For the SignalR hub, set
the ClientTimeoutInterval (default: 30 seconds) and HandshakeTimeout (default: 15
seconds). The following example assumes that KeepAliveInterval uses the default
value of 15 seconds.

) Important

The KeepAliveInterval isn't directly related to the reconnection UI appearing.


The Keep-Alive interval doesn't necessarily need to be changed. If the
reconnection UI appearance issue is due to timeouts, the
ClientTimeoutInterval and HandshakeTimeout can be increased and the
Keep-Alive interval can remain the same. The important consideration is that if
you change the Keep-Alive interval, make sure that the client timeout value is
at least double the value of the Keep-Alive interval and that the Keep-Alive
interval on the client matches the server setting.

In the following example, the ClientTimeoutInterval is increased to 60


seconds, and the HandshakeTimeout is increased to 30 seconds.

For a hosted Blazor WebAssembly app in Program.cs of the Server project:

C#

builder.Services.AddSignalR(options =>
{
options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});

For more information, see ASP.NET Core Blazor SignalR guidance.

Client

Typically, double the value used for the server's KeepAliveInterval to set the
timeout for the client's server timeout (ServerTimeout, default: 30 seconds).
) Important

The Keep-Alive interval (KeepAliveInterval) isn't directly related to the


reconnection UI appearing. The Keep-Alive interval doesn't necessarily need
to be changed. If the reconnection UI appearance issue is due to timeouts, the
server timeout can be increased and the Keep-Alive interval can remain the
same. The important consideration is that if you change the Keep-Alive
interval, make sure that the timeout value is at least double the value of the
Keep-Alive interval and that the Keep-Alive interval on the server matches the
client setting.

In the following example, a custom value of 60 seconds is used for the server
timeout.

When creating a hub connection in a component, set the ServerTimeout (default:


30 seconds) and HandshakeTimeout (default: 15 seconds).

The following example is based on the Index component in the SignalR with
Blazor tutorial. The server timeout is increased to 60 seconds, and the handshake
timeout is increased to 30 seconds:

C#

protected override async Task OnInitializedAsync()


{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.Build();

hubConnection.ServerTimeout = TimeSpan.FromSeconds(60);
hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);

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


...

await hubConnection.StartAsync();
}

When changing the values of the server timeout (ServerTimeout) or the Keep-Alive
interval (KeepAliveInterval:

The server timeout should be at least double the value assigned to the Keep-Alive
interval.
The Keep-Alive interval should be less than or equal to half the value assigned to
the server timeout.
For more information, see ASP.NET Core Blazor SignalR guidance.
Configure the Trimmer for ASP.NET Core
Blazor
Article • 05/05/2023

This article explains how to control the Intermediate Language (IL) Trimmer when
building a Blazor app.

Blazor WebAssembly performs Intermediate Language (IL) trimming to reduce the size
of the published output. By default, trimming occurs when publishing an app.

Trimming may have detrimental effects. In apps that use reflection, the Trimmer often
can't determine the required types for reflection at runtime. To trim apps that use
reflection, the Trimmer must be informed about required types for reflection in both the
app's code and in the packages or frameworks that the app depends on. The Trimmer is
also unable to react to an app's dynamic behavior at runtime. To ensure the trimmed
app works correctly once deployed, test published output frequently while developing.

To configure the Trimmer, see the Trimming options article in the .NET Fundamentals
documentation, which includes guidance on the following subjects:

Disable trimming for the entire app with the <PublishTrimmed> property in the
project file.
Control how aggressively unused IL is discarded by the Trimmer.
Stop the Trimmer from trimming specific assemblies.
"Root" assemblies for trimming.
Surface warnings for reflected types by setting the
<SuppressTrimAnalysisWarnings> property to false in the project file.
Control symbol trimming and debugger support.
Set Trimmer features for trimming framework library features.

Additional resources
Trim self-contained deployments and executables
ASP.NET Core Blazor performance best practices
Deployment layout for ASP.NET Core
Blazor WebAssembly apps
Article • 03/08/2023

This article explains how to enable Blazor WebAssembly deployments in environments


that block the download and execution of dynamic-link library (DLL) files.

Blazor WebAssembly apps require dynamic-link libraries (DLLs) to function, but some
environments block clients from downloading and executing DLLs. In a subset of these
environments, changing the file name extension of DLL files (.dll) is sufficient to bypass
security restrictions, but security products are often able to scan the content of files
traversing the network and block or quarantine DLL files. This article describes one
approach for enabling Blazor WebAssembly apps in these environments, where a
multipart bundle file is created from the app's DLLs so that the DLLs can be downloaded
together bypassing security restrictions.

A hosted Blazor WebAssembly app can customize its published files and packaging of
app DLLs using the following features:

JavaScript initializers that allow customizing the Blazor boot process.


MSBuild extensibility to transform the list of published files and define Blazor
Publish Extensions. Blazor Publish Extensions are files defined during the publish
process that provide an alternative representation for the set of files required to
run a published Blazor WebAssembly app. In this article, a Blazor Publish Extension
is created that produces a multipart bundle with all of the app's DLLs packed into a
single file so that the DLLs can be downloaded together.

The approach demonstrated in this article serves as a starting point for developers to
devise their own strategies and custom loading processes.

2 Warning

Any approach taken to circumvent a security restriction must be carefully


considered for its security implications. We recommend exploring the subject
further with your organization's network security professionals before adopting the
approach in this article. Alternatives to consider include:

Enable security appliances and security software to permit network clients to


download and use the exact files required by a Blazor WebAssembly app.
Switch from the Blazor WebAssembly hosting model to the Blazor Server
hosting model, which maintains all of the app's C# code on the server and
doesn't require downloading DLLs to clients. Blazor Server also offers the
advantage of keeping C# code private without requiring the use of web API
apps for C# code privacy with Blazor WebAssembly apps.

Experimental NuGet package and sample app


The approach described in this article is used by the experimental
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle package
(NuGet.org) . The package contains MSBuild targets to customize the Blazor publish
output and a JavaScript initializer to use a custom boot resource loader, each of which
are described in detail later in this article.

Experimental code (includes the NuGet package reference source and


CustomPackagedApp sample app)

2 Warning

Experimental and preview features are provided for the purpose of collecting
feedback and aren't supported for production use. For more information and to
provide feedback to the ASP.NET Core product unit, see Consider releasing a
supported version of
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle
(dotnet/aspnetcore #36978) .

Later in this article, the Customize the Blazor WebAssembly loading process via a NuGet
package section with its three subsections provide detailed explanations on the
configuration and code in the
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle package. The detailed
explanations are important to understand when you create your own strategy and
custom loading process for Blazor WebAssembly apps. To use the published,
experimental, unsupported NuGet package without customization as a local
demonstration, perform the following steps:

1. Use an existing hosted Blazor WebAssembly solution or create a new solution from
the Blazor WebAssembly project template using Visual Studio or by passing the -
ho|--hosted option to the dotnet new command ( dotnet new blazorwasm -ho ). For
more information, see Tooling for ASP.NET Core Blazor.
2. In the Client project, add the experimental
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle package.

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 .

3. In the Server project, add an endpoint for serving the bundle file ( app.bundle ).
Example code can be found in the Serve the bundle from the host server app
section of this article.

4. Publish the app in Release configuration.

Customize the Blazor WebAssembly loading


process via a NuGet package

2 Warning

The guidance in this section with its three subsections pertains to building a NuGet
package from scratch to implement your own strategy and custom loading process.
The experimental
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle package
(NuGet.org) is based on the guidance in this section. When using the provided
package in a local demonstration of the multipart bundle download approach, you
don't need to follow the guidance in this section. For guidance on how to use the
provided package, see the Experimental NuGet package and sample app section.

Blazor app resources are packed into a multipart bundle file and loaded by the browser
via a custom JavaScript (JS) initializer. For an app consuming the package with the JS
initializer, the app only requires that the bundle file is served when requested. All of the
other aspects of this approach are handled transparently.

Four customizations are required to how a default published Blazor app loads:

An MSBuild task to transform the publish files.


A NuGet package with MSBuild targets that hooks into the Blazor publishing
process, transforms the output, and defines one or more Blazor Publish Extension
files (in this case, a single bundle).
A JS initializer to update the Blazor WebAssembly resource loader callback so that
it loads the bundle and provides the app with the individual files.
A helper on the host Server app to ensure that the bundle is served to clients on
request.

Create an MSBuild task to customize the list of published


files and define new extensions
Create an MSBuild task as a public C# class that can be imported as part of an MSBuild
compilation and that can interact with the build.

The following are required for the C# class:

A new class library project.


A project target framework of netstandard2.0 .
References to MSBuild packages:
Microsoft.Build.Framework
Microsoft.Build.Utilities.Core

7 Note

The NuGet package for the examples in this article are named after the package
provided by Microsoft,
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle . For guidance on
naming and producing your own NuGet package, see the following NuGet articles:

Package authoring best practices


Package ID prefix reservation

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/Microsoft.AspNetC

ore.Components.WebAssembly.MultipartBundle.Tasks.csproj :

XML

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="
{VERSION}" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="
{VERSION}" />
</ItemGroup>

</Project>

Determine the latest package versions for the {VERSION} placeholders at NuGet.org:

Microsoft.Build.Framework
Microsoft.Build.Utilities.Core

To create the MSBuild task, create a public C# class extending


Microsoft.Build.Utilities.Task (not System.Threading.Tasks.Task) and declare three
properties:

PublishBlazorBootStaticWebAsset : The list of files to publish for the Blazor app.

BundlePath : The path where the bundle is written.

Extension : The new Publish Extensions to include in the build.

The following example BundleBlazorAssets class is a starting point for further


customization:

In the Execute method, the bundle is created from the following three file types:
JavaScript files ( dotnet.js )
WASM files ( dotnet.wasm )
App DLLs ( *.dll )
A multipart/form-data bundle is created. Each file is added to the bundle with its
respective descriptions via the Content-Disposition header and the Content-
Type header .
After the bundle is created, the bundle is written to a file.
The build is configured for the extension. The following code creates an extension
item and adds it to the Extension property. Each extension item contains three
pieces of data:
The path to the extension file.
The URL path relative to the root of the Blazor WebAssembly app.
The name of the extension, which groups the files produced by a given
extension.

After accomplishing the preceding goals, the MSBuild task is created for customizing
the Blazor publish output. Blazor takes care of gathering the extensions and making sure
that the extensions are copied to the correct location in the publish output folder (for
example, bin\Release\net6.0\publish ). The same optimizations (for example,
compression) are applied to the JavaScript, WASM, and DLL files as Blazor applies to
other files.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/BundleBlazorAsse

ts.cs :

C#

using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks
{
public class BundleBlazorAssets : Task
{
[Required]
public ITaskItem[]? PublishBlazorBootStaticWebAsset { get; set; }

[Required]
public string? BundlePath { get; set; }

[Output]
public ITaskItem[]? Extension { get; set; }

public override bool Execute()


{
var bundle = new MultipartFormDataContent(
"--0a7e8441d64b4bf89086b85e59523b7d");

foreach (var asset in PublishBlazorBootStaticWebAsset)


{
var name =
Path.GetFileName(asset.GetMetadata("RelativePath"));
var fileContents = File.OpenRead(asset.ItemSpec);
var content = new StreamContent(fileContents);
var disposition = new ContentDispositionHeaderValue("form-
data");
disposition.Name = name;
disposition.FileName = name;
content.Headers.ContentDisposition = disposition;
var contentType = Path.GetExtension(name) switch
{
".js" => "text/javascript",
".wasm" => "application/wasm",
_ => "application/octet-stream"
};
content.Headers.ContentType =
MediaTypeHeaderValue.Parse(contentType);
bundle.Add(content);
}
using (var output = File.Open(BundlePath,
FileMode.OpenOrCreate))
{
output.SetLength(0);

bundle.CopyToAsync(output).ConfigureAwait(false).GetAwaiter()
.GetResult();
output.Flush(true);
}

var bundleItem = new TaskItem(BundlePath);


bundleItem.SetMetadata("RelativePath", "app.bundle");
bundleItem.SetMetadata("ExtensionName", "multipart");

Extension = new ITaskItem[] { bundleItem };

return true;
}
}
}

Author a NuGet package to automatically transform the


publish output
Generate a NuGet package with MSBuild targets that are automatically included when
the package is referenced:

Create a new Razor class library (RCL) project.


Create a targets file following NuGet conventions to automatically import the
package in consuming projects. For example, create build\net6.0\{PACKAGE
ID}.targets , where {PACKAGE ID} is the package identifier of the package.

Collect the output from the class library containing the MSBuild task and confirm
the output is packed in the right location.
Add the necessary MSBuild code to attach to the Blazor pipeline and invoke the
MSBuild task to generate the bundle.

The approach described in this section only uses the package to deliver targets and
content, which is different from most packages where the package includes a library
DLL.

2 Warning

The sample package described in this section demonstrates how to customize the
Blazor publish process. The sample NuGet package is for use as a local
demonstration only. Using this package in production is not supported.
7 Note

The NuGet package for the examples in this article are named after the package
provided by Microsoft,
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle . For guidance on

naming and producing your own NuGet package, see the following NuGet articles:

Package authoring best practices


Package ID prefix reservation

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/Microsoft.AspNetCore.Co

mponents.WebAssembly.MultipartBundle.csproj :

XML

<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<NoWarn>NU5100</NoWarn>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Description>
Sample demonstration package showing how to customize the Blazor
publish
process. Using this package in production is not supported!
</Description>
<IsPackable>true</IsPackable>
<IsShipping>true</IsShipping>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>

<ItemGroup>
<None Update="build\**"
Pack="true"
PackagePath="%(Identity)" />
<Content Include="_._"
Pack="true"
PackagePath="lib\net6.0\_._" />
</ItemGroup>

<Target Name="GetTasksOutputDlls"
BeforeTargets="CoreCompile">
<MSBuild
Projects="..\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tas
ks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj"
Targets="Publish;PublishItemsOutputGroup"
Properties="Configuration=Release">
<Output TaskParameter="TargetOutputs"
ItemName="_TasksProjectOutputs" />
</MSBuild>
<ItemGroup>
<Content Include="@(_TasksProjectOutputs)"
Condition="'%(_TasksProjectOutputs.Extension)' == '.dll'"
Pack="true"
PackagePath="tasks\%(_TasksProjectOutputs.TargetPath)"
KeepMetadata="Pack;PackagePath" />
</ItemGroup>
</Target>

</Project>

7 Note

The <NoWarn>NU5100</NoWarn> property in the preceding example suppresses the


warning about the assemblies placed in the tasks folder. For more information, see
NuGet Warning NU5100.

Add a .targets file to wire up the MSBuild task to the build pipeline. In this file, the
following goals are accomplished:

Import the task into the build process. Note that the path to the DLL is relative to
the ultimate location of the file in the package.
The ComputeBlazorExtensionsDependsOn property attaches the custom target to the
Blazor WebAssembly pipeline.
Capture the Extension property on the task output and add it to
BlazorPublishExtension to tell Blazor about the extension. Invoking the task in the

target produces the bundle. The list of published files is provided by the Blazor
WebAssembly pipeline in the PublishBlazorBootStaticWebAsset item group. The
bundle path is defined using the IntermediateOutputPath (typically inside the obj
folder). Ultimately, the bundle is copied automatically to the correct location in the
publish output folder (for example, bin\Release\net6.0\publish ).

When the package is referenced, it generates a bundle of the Blazor files during publish.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/build/net6.0/Microsoft.

AspNetCore.Components.WebAssembly.MultipartBundle.targets :

XML

<Project>
<UsingTask
TaskName="Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.
BundleBlazorAssets"

AssemblyFile="$(MSBuildThisProjectFileDirectory)..\..\tasks\Microsoft.AspNet
Core.Components.WebAssembly.MultipartBundle.Tasks.dll" />

<PropertyGroup>
<ComputeBlazorExtensionsDependsOn>
$(ComputeBlazorExtensionsDependsOn);_BundleBlazorDlls
</ComputeBlazorExtensionsDependsOn>
</PropertyGroup>

<Target Name="_BundleBlazorDlls">
<BundleBlazorAssets
PublishBlazorBootStaticWebAsset="@(PublishBlazorBootStaticWebAsset)"
BundlePath="$(IntermediateOutputPath)bundle.multipart">
<Output TaskParameter="Extension"
ItemName="BlazorPublishExtension"/>
</BundleBlazorAssets>
</Target>

</Project>

Automatically bootstrap Blazor from the bundle


The NuGet package leverages JavaScript (JS) initializers to automatically bootstrap a
Blazor WebAssembly app from the bundle instead of using individual DLL files. JS
initializers are used to change the Blazor boot resource loader and use the bundle.

To create a JS initializer, add a JS file with the name {NAME}.lib.module.js to the


wwwroot folder of the package project, where the {NAME} placeholder is the package
identifier. For example, the file for the Microsoft package is named
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js . The

exported functions beforeStart and afterStarted handle loading.

The JS initializers:

Detect if the Publish Extension is available by checking for extensions.multipart ,


which is the extension name ( ExtensionName ) provided in the Create an MSBuild
task to customize the list of published files and define new extensions section.
Download the bundle and parse the contents into a resources map using
generated object URLs.
Update the boot resource loader (options.loadBootResource) with a custom
function that resolves the resources using the object URLs.
After the app has started, revoke the object URLs to release memory in the
afterStarted function.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNe

tCore.Components.WebAssembly.MultipartBundle.lib.module.js :

JavaScript

const resources = new Map();

export async function beforeStart(options, extensions) {


if (!extensions || !extensions.multipart) {
return;
}

try {
const integrity = extensions.multipart['app.bundle'];
const bundleResponse =
await fetch('app.bundle', { integrity: integrity, cache: 'no-cache'
});
const bundleFromData = await bundleResponse.formData();
for (let value of bundleFromData.values()) {
resources.set(value, URL.createObjectURL(value));
}
options.loadBootResource = function (type, name, defaultUri, integrity)
{
return resources.get(name) ?? null;
}
} catch (error) {
console.log(error);
}
}

export async function afterStarted(blazor) {


for (const [_, url] of resources) {
URL.revokeObjectURL(url);
}
}

Serve the bundle from the host server app


Due to security restrictions, ASP.NET Core doesn't serve the app.bundle file by default. A
request processing helper is required to serve the file when it's requested by clients.

7 Note

Since the same optimizations are transparently applied to the Publish Extensions
that are applied to the app's files, the app.bundle.gz and app.bundle.br
compressed asset files are produced automatically on publish.

Place C# code in Program.cs of the Server project immediately before the line that sets
the fallback file to index.html ( app.MapFallbackToFile("index.html"); ) to respond to a
request for the bundle file (for example, app.bundle ):

C#

app.MapGet("app.bundle", (HttpContext context) =>


{
string? contentEncoding = null;
var contentType =
"multipart/form-data; boundary=\"-
-0a7e8441d64b4bf89086b85e59523b7d\"";
var fileName = "app.bundle";

var acceptEncodings = context.Request.Headers.AcceptEncoding;

if (Microsoft.Net.Http.Headers.StringWithQualityHeaderValue
.StringWithQualityHeaderValue
.TryParseList(acceptEncodings, out var encodings))
{
if (encodings.Any(e => e.Value == "br"))
{
contentEncoding = "br";
fileName += ".br";
}
else if (encodings.Any(e => e.Value == "gzip"))
{
contentEncoding = "gzip";
fileName += ".gz";
}
}

if (contentEncoding != null)
{
context.Response.Headers.ContentEncoding = contentEncoding;
}

return Results.File(
app.Environment.WebRootFileProvider.GetFileInfo(fileName)
.CreateReadStream(), contentType);
});

The content type matches the type defined earlier in the build task. The endpoint checks
for the content encodings accepted by the browser and serves the optimal file, Brotli
( .br ) or Gzip ( .gz ).
Multiple hosted ASP.NET Core Blazor
WebAssembly apps
Article • 05/12/2023

This article explains how to configure a hosted Blazor WebAssembly app to host
multiple Blazor WebAssembly apps.

Configuration
Select the version of this article that matches your hosting requirements, either
port/domain hosting (for example, :5001 / :5002 or firstapp.com / secondapp.com ) or
route subpath hosting (for example, /FirstApp and /SecondApp ).

With the current hosting selection, this article covers port/domain hosting (for example,
:5001 / :5002 or firstapp.com / secondapp.com ).

In the following examples:

The project name of the hosted Blazor WebAssembly app is MultipleBlazorApps in


a folder named MultipleBlazorApps .
The three projects in the solution before a second client app is added are
MultipleBlazorApps.Client in the Client folder, MultipleBlazorApps.Server in the
Server folder, and MultipleBlazorApps.Shared in the Shared folder.

The initial (first) client app is the default client project of the solution created from
the Blazor WebAssembly project template.
A second client app is added to the solution, MultipleBlazorApps.SecondClient in a
folder named SecondClient .
Optionally, the server project ( MultipleBlazorApps.Server ) can serve pages or
views as a Razor Pages or MVC app.
The first client app is accessible in a browser at port 5001 or with a host of
firstapp.com . The second client app is accessible in a browser at port 5002 or with

a host of secondapp.com .

The examples shown in this article require additional configuration for:

Accessing the apps directly at the example host domains, firstapp.com and
secondapp.com .
Certificates for the client apps to enable TLS/HTTPS security.
Configuring the server app as a Razor Pages app for the following features:
Integration of Razor components into pages or views.
Prerendering Razor components.

The preceding configurations are beyond the scope of this article. For more information,
see the following resources:

Host and deploy articles


Enforce HTTPS in ASP.NET Core
Prerender and integrate ASP.NET Core Razor components

Use an existing hosted Blazor WebAssembly solution or create a new hosted Blazor
WebAssembly solution from the Blazor WebAssembly project template by passing the -
ho|--hosted option if using the .NET CLI or selecting the ASP.NET Core Hosted

checkbox in Visual Studio or Visual Studio for Mac when the project is created in the
IDE.

Use a folder for the solution named MultipleBlazorApps and name the project
MultipleBlazorApps .

Create a new folder in the solution named SecondClient . In the new folder, add a
second Blazor WebAssembly client app named MultipleBlazorApps.SecondClient . Add
the project as a standalone Blazor WebAssembly app. To create a standalone Blazor
WebAssembly app, don't pass the -ho|--hosted option if using the .NET CLI or don't use
the ASP.NET Core Hosted checkbox if using Visual Studio.

Make the following changes to the MultipleBlazorApps.SecondClient project:

Copy the FetchData component ( Pages/FetchData.razor ) from the Client/Pages


folder to the SecondClient/Pages folder. This step is required because a standalone
Blazor WebAssembly app doesn't call a Server project's controller for weather
data, it uses a static data file. By copying the FetchData component to the added
project, the second client app also makes a web API call to the server API for
weather data.
Delete the SecondClient/wwwroot/sample-data folder, as the weather.json file in
the folder isn't used.

The following table describes the solution's folders and project names after the
SecondClient folder and MultipleBlazorApps.SecondClient project are added.

Physical folder Project name Description

Client MultipleBlazorApps.Client Blazor WebAssembly client app


Physical folder Project name Description

SecondClient MultipleBlazorApps.SecondClient Blazor WebAssembly client app

Server MultipleBlazorApps.Server ASP.NET Core server app

Shared MultipleBlazorApps.Shared Shared resources project

The MultipleBlazorApps.Server project serves the two Blazor WebAssembly client apps
and provides weather data to the client apps' FetchData components via an MVC
controller. Optionally, the MultipleBlazorApps.Server project can also serve pages or
views, as a traditional Razor Pages or MVC app. Steps to enable serving pages or views
are covered later in this article.

7 Note

The demonstration in this article uses static web asset path names of FirstApp for
the MultipleBlazorApps.Client project and SecondApp for the
MultipleBlazorApps.SecondClient project. The names " FirstApp " and " SecondApp "
are merely for demonstration purposes. Other names are acceptable to distinguish
the client apps, such as App1 / App2 , Client1 / Client2 , 1 / 2 , or any similar naming
scheme.

When routing requests to the client apps by a port or a domain, " FirstApp " and
" SecondApp " are used internally to route requests and serve responses for static
assets and aren't seen in the browser's address bar.

In the first client app's project file ( MultipleBlazorApps.Client.csproj ), add a


<StaticWebAssetBasePath> property to a <PropertyGroup> with a value of FirstApp to
set the base path for the project's static assets:

XML

<StaticWebAssetBasePath>FirstApp</StaticWebAssetBasePath>

In the MultipleBlazorApps.SecondClient app's project file


( MultipleBlazorApps.SecondClient.csproj ):

Add a <StaticWebAssetBasePath> property to a <PropertyGroup> with a value of


SecondApp :

XML
<StaticWebAssetBasePath>SecondApp</StaticWebAssetBasePath>

Add a project reference for the MultipleBlazorApps.Shared project to an


<ItemGroup> :

XML

<ItemGroup>
<ProjectReference
Include="..\Shared\MultipleBlazorApps.Shared.csproj" />
</ItemGroup>

In the server app's project file ( Server/MultipleBlazorApps.Server.csproj ), create a


project reference for the added MultipleBlazorApps.SecondClient client app in an
<ItemGroup> :

XML

<ProjectReference
Include="..\SecondClient\MultipleBlazorApps.SecondClient.csproj" />

In the server app's Properties/launchSettings.json file, configure the applicationUrl


of the Kestrel profile ( MultipleBlazorApps.Server ) to access the client apps at ports 5001
and 5002. If you configure your local environment to use the example domains, URLs for
applicationUrl can use firstapp.com and secondapp.com and not use the ports.

7 Note

The use of ports in this demonstration allows access to the client projects in a local
browser without the need to configure a local hosting environment so that web
browsers can access the client apps via the host configurations, firstapp.com and
secondapp.com . In production scenarios, a typical configuration is to use

subdomains to distinguish the client apps.

For example:

The ports are dropped from the configuration of this demonstration.


The hosts are changed to use subdomains, such as www.contoso.com for site
visitors and admin.contoso.com for administrators.
Additional hosts can be included for additional client apps, and at least one
more host is required if the server app is also a Razor Pages or MVC app that
serves pages or views.

If you plan to serve pages or views from the server app, use the following
applicationUrl setting in the Properties/launchSettings.json file, which permits the

following access:

Optionally, the Razor Pages or MVC app ( MultipleBlazorApps.Server project)


responds to requests at port 5000.
Responses to requests for the first client ( MultipleBlazorApps.Client project) are
at port 5001.
Responses to requests for the second client ( MultipleBlazorApps.SecondClient
project) are at port 5002.

JSON

"applicationUrl":
"https://localhost:5000;https://localhost:5001;https://localhost:5002",

If you don't plan for the server app to serve pages or views and only serve the Blazor
WebAssembly client apps, use the following setting, which permits the following access:

The first client app responds on port 5001.


The second client app responds on port 5002.

JSON

"applicationUrl": "https://localhost:5001;https://localhost:5002",

In the server app's Program.cs file, remove the following code, which appears after the
call to UseHttpsRedirection:

If you plan to serve pages or views from the server app, delete the following lines
of code:

diff

- app.UseBlazorFrameworkFiles();

diff

- app.MapFallbackToFile("index.html");
If you plan for the server app to only serve the Blazor WebAssembly client apps,
delete the following code:

diff

- app.UseBlazorFrameworkFiles();

...

- app.UseRouting();

- app.MapRazorPages();
- app.MapControllers();
- app.MapFallbackToFile("index.html");

Leave Static File Middleware in place:

C#

app.UseStaticFiles();

Add middleware that maps requests to the client apps. The following example
configures the middleware to run when the request port is either 5001 for the first
client app or 5002 for the second client app, or the request host is either
firstapp.com for the first client app or secondapp.com for the second client app.

7 Note

Use of the hosts ( firstapp.com / secondapp.com ) on a local system with a local


browser requires additional configuration that's beyond the scope of this
article. For local testing of this scenario, we recommend using ports. Typical
production apps are configured to use subdomains, such as www.contoso.com
for site visitors and admin.contoso.com for administrators. With the proper
DNS and server configuration, which is beyond the scope of this article and
depends on the technologies used, the app responds to requests at whatever
hosts are named in the following code.

Where you removed the app.UseBlazorFrameworkFiles(); line from Program.cs ,


place the following code:

C#

app.MapWhen(ctx => ctx.Request.Host.Port == 5001 ||


ctx.Request.Host.Equals("firstapp.com"), first =>
{
first.Use((ctx, nxt) =>
{
ctx.Request.Path = "/FirstApp" + ctx.Request.Path;
return nxt();
});

first.UseBlazorFrameworkFiles("/FirstApp");
first.UseStaticFiles();
first.UseStaticFiles("/FirstApp");
first.UseRouting();

first.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToFile("/FirstApp/{*path:nonfile}",
"FirstApp/index.html");
});
});

app.MapWhen(ctx => ctx.Request.Host.Port == 5002 ||


ctx.Request.Host.Equals("secondapp.com"), second =>
{
second.Use((ctx, nxt) =>
{
ctx.Request.Path = "/SecondApp" + ctx.Request.Path;
return nxt();
});

second.UseBlazorFrameworkFiles("/SecondApp");
second.UseStaticFiles();
second.UseStaticFiles("/SecondApp");
second.UseRouting();

second.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToFile("/SecondApp/{*path:nonfile}",
"SecondApp/index.html");
});
});

2 Warning

API that relies on the Host header , such as HttpRequest.Host, is subject to


potential spoofing by clients.

In high security scenarios, consider assigning a value to HttpRequest.Host in


middleware before UseRouting is called or using HttpContext.Connection
(ConnectionInfo.LocalPort) where the ports are checked in the preceding
example.
For more information on UseStaticFiles, see ASP.NET Core Blazor static files.

For more information on UseBlazorFrameworkFiles and MapFallbackToFile , see the


following resources:

Microsoft.AspNetCore.Builder.ComponentsWebAssemblyApplicationBuilderExtensi
ons.UseBlazorFrameworkFiles (reference source )
Microsoft.AspNetCore.Builder.StaticFilesEndpointRouteBuilderExtensions.MapFallba
ckToFile (reference source )

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

Requests from the client apps to /WeatherForecast in the server API are either to
/FirstApp/WeatherForecast or /SecondApp/WeatherForecast depending on which client
app makes the request. Therefore, the controller routes that return weather data from
the server API require a modification to include the path segments.

In the server app's weather forecast controller


( Controllers/WeatherForecastController.cs ), replace the existing route ( [Route("
[controller]")] ) to WeatherForecastController with the following routes, which take
into account the client request paths:

C#

[Route("FirstApp/[controller]")]
[Route("SecondApp/[controller]")]

If you plan to serve pages from the server app, add an Index Razor page to the Pages
folder of the server app:

Pages/Index.cshtml :

CSHTML

@page
@model MultipleBlazorApps.Server.Pages.IndexModel
@{
ViewData["Title"] = "Home";
}

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Home</title>
</head>
<body>
<div class="main">
<div class="content px-4">

<div>
<h1>Welcome</h1>
<p>Hello from Razor Pages!</p>
</div>
</div>
</div>
</body>
</html>

Pages/Index.cshtml.cs :

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MultipleBlazorApps.Server.Pages;

public class IndexModel : PageModel


{
public void OnGet()
{
}
}

7 Note

The preceding Index page is a minimal example purely for demonstration


purposes. If the app requires additional Razor Pages assets, such as a layout, styles,
scripts, and imports, obtain them from an app created from the Razor Pages project
template. For more information, see Introduction to Razor Pages in ASP.NET Core.

If you plan to serve MVC views from the server app, add an Index view and a Home
controller:
Views/Home/Index.cshtml :

CSHTML

@{
ViewData["Title"] = "Home";
}

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Home</title>
</head>
<body>
<div class="main">
<div class="content px-4">

<div>
<h1>Welcome</h1>
<p>Hello from MVC!</p>
</div>
</div>
</div>
</body>
</html>

Controllers/HomeController.cs :

C#

using Microsoft.AspNetCore.Mvc;

namespace MultipleBlazorApps.Server.Controllers;

public class HomeController : Controller


{
public IActionResult Index()
{
return View();
}
}

7 Note

The preceding Index view is a minimal example purely for demonstration purposes.
If the app requires additional MVC assets, such as a layout, styles, scripts, and
imports, obtain them from an app created from the MVC project template. For
more information, see Get started with ASP.NET Core MVC.
For more information on using the Razor components from either of the client apps in
pages or views of the server app, see Prerender and integrate ASP.NET Core Razor
components.

Run the app


Run the MultipleBlazorApps.Server project:

Access the initial client app at https://localhost:5001 .


Access the added client app at https://localhost:5002 .
If the server app is configured to serve pages or views, access the Index page or
view at https://localhost:5000 .

) Important

When running the app with the dotnet run command (.NET CLI), confirm that the
command shell is open in the Server folder of the solution.

When using Visual Studio's start button to run the app, confirm that the
MultipleBlazorApps.Server project is set as the startup project (highlighted in

Solution Explorer).

Static assets
When an asset is in a client app's wwwroot folder, provide the static asset request path in
components:

razor

<img alt="..." src="{PATH AND FILE NAME}" />

The {PATH AND FILE NAME} placeholder is the path and file name under wwwroot .

For example, the source for a Jeep image ( jeep-yj.png ) in the vehicle folder of
wwwroot :

razor

<img alt="Jeep Wrangler YJ" src="vehicle/jeep-yj.png" />


Razor class library (RCL) support
Add the Razor class library (RCL) to the solution as a new project:

Right-click the solution in Solution Explorer and select Add > New Project.
Use the Razor Class Library project template to create the project. The examples in
this section use the project name ComponentLibrary , which is also the RCL's
assembly name. Do not select the Support pages and views checkbox.

For each hosted Blazor WebAssembly client app, create a project reference for the RCL
project by right-clicking each client project in Solution Explorer and selecting Add >
Project Reference.

Use components from the RCL in the client apps with either of the following
approaches:

Place an @using directive at the top of the component for the RCL's namespace
and add Razor syntax for the component. The following example is for an RCL with
the assembly name ComponentLibrary :

razor

@using ComponentLibrary

...

<Component1 />

Provide the RCL's namespace along with the Razor syntax for the component. This
approach doesn't require an @using directive at the top of the component file. The
following example is for an RCL with the assembly name ComponentLibrary :

razor

<ComponentLibrary.Component1 />

7 Note

An @using directive can also be placed into each client app's _Import.razor file,
which makes the RCL's namespace globally available to components in that project.

When any other static asset is in the wwwroot folder of an RCL, reference the static asset
in a client app per the guidance in Reusable Razor UI in class libraries with ASP.NET
Core:

razor

<img alt="..." src="_content/{PACKAGE ID}/{PATH AND FILE NAME}" />

The {PACKAGE ID} placeholder is the RCL's package ID. The package ID defaults to the
project's assembly name if <PackageId> isn't specified in the project file. The {PATH AND
FILE NAME} placeholder is path and file name under wwwroot .

The following example shows the markup for a Jeep image ( jeep-yj.png ) in the vehicle
folder of the RCL's wwwroot folder. The following example is for an RCL with the
assembly name ComponentLibrary :

razor

<img alt="Jeep Wrangler YJ" src="_content/ComponentLibrary/vehicle/jeep-


yj.png" />

Additional resources
Consume ASP.NET Core Razor components from a Razor class library (RCL)
Reusable Razor UI in class libraries with ASP.NET Core
ASP.NET Core Blazor CSS isolation
ASP.NET Core Blazor Server with Entity
Framework Core (EF Core)
Article • 05/09/2023

This article explains how to use Entity Framework Core (EF Core) in Blazor Server apps.

Blazor Server is a stateful app framework. The app maintains an ongoing connection to
the server, and the user's state is held in the server's memory in a circuit. One example
of user state is data held in dependency injection (DI) service instances that are scoped
to the circuit. The unique application model that Blazor Server provides requires a
special approach to use Entity Framework Core.

7 Note

This article addresses EF Core in Blazor Server apps. Blazor WebAssembly apps run
in a WebAssembly sandbox that prevents most direct database connections.
Running EF Core in Blazor WebAssembly is beyond the scope of this article.

Sample app
The sample app was built as a reference for Blazor Server apps that use EF Core. The
sample app includes a grid with sorting and filtering, delete, add, and update
operations. The sample demonstrates use of EF Core to handle optimistic concurrency.

View or download sample code (how to download)

The sample uses a local SQLite database so that it can be used on any platform. The
sample also configures database logging to show the SQL queries that are generated.
This is configured in appsettings.Development.json :

JSON

{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}

The grid, add, and view components use the "context-per-operation" pattern, where a
context is created for each operation. The edit component uses the "context-per-
component" pattern, where a context is created for each component.

7 Note

Some of the code examples in this topic require namespaces and services that
aren't shown. To inspect the fully working code, including the required @using and
@inject directives for Razor examples, see the sample app .

Database access
EF Core relies on a DbContext as the means to configure database access and act as a
unit of work . EF Core provides the AddDbContext extension for ASP.NET Core apps
that registers the context as a scoped service by default. In Blazor Server apps, scoped
service registrations can be problematic because the instance is shared across
components within the user's circuit. DbContext isn't thread safe and isn't designed for
concurrent use. The existing lifetimes are inappropriate for these reasons:

Singleton shares state across all users of the app and leads to inappropriate
concurrent use.
Scoped (the default) poses a similar issue between components for the same user.
Transient results in a new instance per request; but as components can be long-
lived, this results in a longer-lived context than may be intended.

The following recommendations are designed to provide a consistent approach to using


EF Core in Blazor Server apps.

By default, consider using one context per operation. The context is designed for
fast, low overhead instantiation:

C#

using var context = new MyContext();

return await context.MyEntities.ToListAsync();

Use a flag to prevent multiple concurrent operations:


C#

if (Loading)
{
return;
}

try
{
Loading = true;

...
}
finally
{
Loading = false;
}

Place operations after the Loading = true; line in the try block.

Loading logic doesn't require locking database records because thread safety isn't
a concern. The loading logic is used to disable UI controls so that users don't
inadvertently select buttons or update fields while data is fetched.

If there's any chance that multiple threads may access the same code block, inject
a factory and make a new instance per operation. Otherwise, injecting and using
the context is usually sufficient.

For longer-lived operations that take advantage of EF Core's change tracking or


concurrency control, scope the context to the lifetime of the component.

New DbContext instances


The fastest way to create a new DbContext instance is by using new to create a new
instance. However, there are scenarios that require resolving additional dependencies:

Using DbContextOptions to configure the context.


Using a connection string per DbContext, such as when you use ASP.NET Core's
Identity model. For more information, see Multi-tenancy (EF Core documentation).

The recommended approach to create a new DbContext with dependencies is to use a


factory. EF Core 5.0 or later provides a built-in factory for creating new contexts.

The following example configures SQLite and enables data logging. The code uses an
extension method ( AddDbContextFactory ) to configure the database factory for DI and
provide default options:
C#

builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

The factory is injected into components and used to create new DbContext instances.

In Pages/Index.razor of the sample app , IDbContextFactory<ContactContext> is


injected into the component:

razor

@inject IDbContextFactory<ContactContext> DbFactory

A DbContext is created using the factory ( DbFactory ) to delete a contact in the


DeleteContactAsync method:

C#

private async Task DeleteContactAsync()


{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;

if (Wrapper is not null && context.Contacts is not null)


{
var contact = await context.Contacts
.FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

if (contact is not null)


{
context.Contacts?.Remove(contact);
await context.SaveChangesAsync();
}
}

Filters.Loading = false;

await ReloadAsync();
}

7 Note

Filters is an injected IContactFilters , and Wrapper is a component reference to

the GridWrapper component. See the Index component ( Pages/Index.razor ) in the


sample app .
Scope to the component lifetime
You may wish to create a DbContext that exists for the lifetime of a component. This
allows you to use it as a unit of work and take advantage of built-in features, such as
change tracking and concurrency resolution. You can use the factory to create a context
and track it for the lifetime of the component. First, implement IDisposable and inject
the factory as shown in Pages/EditContact.razor :

razor

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

The sample app ensures the context is disposed when the component is disposed:

C#

public void Dispose()


{
Context?.Dispose();
}

Finally, OnInitializedAsync is overridden to create a new context. In the sample app,


OnInitializedAsync loads the contact in the same method:

C#

protected override async Task OnInitializedAsync()


{
Busy = true;

try
{
Context = DbFactory.CreateDbContext();

if (Context is not null && Context.Contacts is not null)


{
var contact = await Context.Contacts.SingleOrDefaultAsync(c =>
c.Id == ContactId);

if (contact is not null)


{
Contact = contact;
}
}
}
finally
{
Busy = false;
}

await base.OnInitializedAsync();
}

Enable sensitive data logging


EnableSensitiveDataLogging includes application data in exception messages and
framework logging. The logged data can include the values assigned to properties of
entity instances and parameter values for commands sent to the database. Logging data
with EnableSensitiveDataLogging is a security risk, as it may expose passwords and
other personally identifiable information (PII) when it logs SQL statements executed
against the database.

We recommend only enabling EnableSensitiveDataLogging for development and


testing:

C#

#if DEBUG
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
.EnableSensitiveDataLogging());
#else
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source=
{nameof(ContactContext.ContactsDb)}.db"));
#endif

Additional resources
EF Core documentation
Blazor samples GitHub repository (dotnet/blazor-samples)
ASP.NET Core Blazor advanced scenarios
(render tree construction)
Article • 05/03/2023

This article describes the advanced scenario for building Blazor render trees manually
with RenderTreeBuilder.

2 Warning

Use of RenderTreeBuilder to create components is an advanced scenario. A


malformed component (for example, an unclosed markup tag) can result in
undefined behavior. Undefined behavior includes broken content rendering, loss of
app features, and compromised security.

Manually build a render tree


( RenderTreeBuilder )
RenderTreeBuilder provides methods for manipulating components and elements,
including building components manually in C# code.

Consider the following PetDetails component, which can be manually rendered in


another component.

Shared/PetDetails.razor :

razor

<h2>Pet Details</h2>

<p>@PetDetailsQuote</p>

@code
{
[Parameter]
public string? PetDetailsQuote { get; set; }
}

In the following BuiltContent component, the loop in the CreateComponent method


generates three PetDetails components.
In RenderTreeBuilder methods with a sequence number, sequence numbers are source
code line numbers. The Blazor difference algorithm relies on the sequence numbers
corresponding to distinct lines of code, not distinct call invocations. When creating a
component with RenderTreeBuilder methods, hardcode the arguments for sequence
numbers. Using a calculation or counter to generate the sequence number can lead to
poor performance. For more information, see the Sequence numbers relate to code line
numbers and not execution order section.

Pages/BuiltContent.razor :

razor

@page "/built-content"

<h1>Build a component</h1>

<div>
@CustomRender
</div>

<button @onclick="RenderComponent">
Create three Pet Details components
</button>

@code {
private RenderFragment? CustomRender { get; set; }

private RenderFragment CreateComponent() => builder =>


{
for (var i = 0; i < 3; i++)
{
builder.OpenComponent(0, typeof(PetDetails));
builder.AddAttribute(1, "PetDetailsQuote", "Someone's best
friend!");
builder.CloseComponent();
}
};

private void RenderComponent()


{
CustomRender = CreateComponent();
}
}

2 Warning

The types in Microsoft.AspNetCore.Components.RenderTree allow processing of


the results of rendering operations. These are internal details of the Blazor
framework implementation. These types should be considered unstable and subject
to change in future releases.

Sequence numbers relate to code line numbers and not


execution order
Razor component files ( .razor ) are always compiled. Executing compiled code has a
potential advantage over interpreting code because the compile step that yields the
compiled code can be used to inject information that improves app performance at
runtime.

A key example of these improvements involves sequence numbers. Sequence numbers


indicate to the runtime which outputs came from which distinct and ordered lines of
code. The runtime uses this information to generate efficient tree diffs in linear time,
which is far faster than is normally possible for a general tree diff algorithm.

Consider the following Razor component file ( .razor ):

razor

@if (someFlag)
{
<text>First</text>
}

Second

The preceding Razor markup and text content compiles into C# code similar to the
following:

C#

if (someFlag)
{
builder.AddContent(0, "First");
}

builder.AddContent(1, "Second");

When the code executes for the first time and someFlag is true , the builder receives the
sequence in the following table.

Sequence Type Data


Sequence Type Data

0 Text node First

1 Text node Second

Imagine that someFlag becomes false and the markup is rendered again. This time, the
builder receives the sequence in the following table.

Sequence Type Data

1 Text node Second

When the runtime performs a diff, it sees that the item at sequence 0 was removed, so
it generates the following trivial edit script with a single step:

Remove the first text node.

The problem with generating sequence numbers


programmatically
Imagine instead that you wrote the following render tree builder logic:

C#

var seq = 0;

if (someFlag)
{
builder.AddContent(seq++, "First");
}

builder.AddContent(seq++, "Second");

The first output is reflected in the following table.

Sequence Type Data

0 Text node First

1 Text node Second

This outcome is identical to the prior case, so no negative issues exist. someFlag is false
on the second rendering, and the output is seen in the following table.
Sequence Type Data

0 Text node Second

This time, the diff algorithm sees that two changes have occurred. The algorithm
generates the following edit script:

Change the value of the first text node to Second .


Remove the second text node.

Generating the sequence numbers has lost all the useful information about where the
if/else branches and loops were present in the original code. This results in a diff twice

as long as before.

This is a trivial example. In more realistic cases with complex and deeply nested
structures, and especially with loops, the performance cost is usually higher. Instead of
immediately identifying which loop blocks or branches have been inserted or removed,
the diff algorithm must recurse deeply into the render trees. This usually results in
building longer edit scripts because the diff algorithm is misinformed about how the old
and new structures relate to each other.

Guidance and conclusions


App performance suffers if sequence numbers are generated dynamically.
The framework can't create its own sequence numbers automatically at runtime
because the necessary information doesn't exist unless it's captured at compile
time.
Don't write long blocks of manually-implemented RenderTreeBuilder logic. Prefer
.razor files and allow the compiler to deal with the sequence numbers. If you're

unable to avoid manual RenderTreeBuilder logic, split long blocks of code into
smaller pieces wrapped in OpenRegion/CloseRegion calls. Each region has its own
separate space of sequence numbers, so you can restart from zero (or any other
arbitrary number) inside each region.
If sequence numbers are hardcoded, the diff algorithm only requires that sequence
numbers increase in value. The initial value and gaps are irrelevant. One legitimate
option is to use the code line number as the sequence number, or start from zero
and increase by ones or hundreds (or any preferred interval).
Blazor uses sequence numbers, while other tree-diffing UI frameworks don't use
them. Diffing is far faster when sequence numbers are used, and Blazor has the
advantage of a compile step that deals with sequence numbers automatically for
developers authoring .razor files.
Overview of Single Page Applications
(SPA) in ASP.NET Core
Article • 02/14/2023

Architecture of Single Page Application


templates
The Single Page Application (SPA) templates for Angular and React offer the ability
to develop Angular and React apps that are hosted inside a .NET backend server.

At publish time, the files of the Angular and React app are copied to the wwwroot folder
and are served via the static files middleware.

Rather than returning HTTP 404 (Not Found), a fallback route handles unknown requests
to the backend and serves the index.html for the SPA.

During development, the app is configured to use the frontend proxy. React and
Angular use the same frontend proxy.

When the app launches, the index.html page is opened in the browser. A special
middleware that is only enabled in development:

Intercepts the incoming requests.


Checks whether the proxy is running.
Redirects to the URL for the proxy if it's running or launches a new instance of the
proxy.
Returns a page to the browser that auto refreshes every few seconds until the
proxy is up and the browser is redirected.
The primary benefit the ASP.NET Core SPA templates provide:

Launches a proxy if it's not already running.


Setting up HTTPS.
Configuring some requests to be proxied to the backend ASP.NET Core server.

When the browser sends a request for a backend endpoint, for example
/weatherforecast in the templates. The SPA proxy receives the request and sends it

back to the server transparently. The server responds and the SPA proxy sends the
request back to the browser:

Published Single Page Apps


When the app is published, the SPA becomes a collection of files in the wwwroot folder.

There is no runtime component required to serve the app:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");

app.MapFallbackToFile("index.html");

app.Run();

In the preceding template generated Program.cs file:

app. UseStaticFiles allows the files to be served.


app. MapFallbackToFile ("index.html") enables serving the default document for

any unknown request the server receives.

When the app is published with dotnet publish, the following tasks in the csproj file
ensures that npm restore runs and that the appropriate npm script runs to generate
the production artifacts:

XML

<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition="


'$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to
build and run this project. To continue, please install Node.js from
https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'.
This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>

<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">


<!-- As part of publishing, ensure the JS resources are freshly built in
production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

<!-- Include the newly-built files in the publish output -->


<ItemGroup>
<DistFiles Include="$(SpaRoot)build\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')"
Exclude="@(ResolvedFileToPublish)">
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)
</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>
Developing Single Page Apps
The project file defines a few properties that control the behavior of the app during
development:

XML

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>
<SpaRoot>ClientApp\</SpaRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**
</DefaultItemExcludes>
<SpaProxyServerUrl>https://localhost:44414</SpaProxyServerUrl>
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaProxy"
Version="7.0.1" />
</ItemGroup>

<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project
files list -->
<Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>

<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition="


'$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to
build and run this project. To continue, please install Node.js from
https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'.
This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>

<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">


<!-- As part of publishing, ensure the JS resources are freshly built in
production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

<!-- Include the newly-built files in the publish output -->


<ItemGroup>
<DistFiles Include="$(SpaRoot)build\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')"
Exclude="@(ResolvedFileToPublish)">
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)
</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

SpaProxyServerUrl : Controls the URL where the server expects the SPA proxy to be
running. This is the URL:
The server pings after launching the proxy to know if it's ready.
Where it redirects the browser after a successful response.
SpaProxyLaunchCommand : The command the server uses to launch the SPA proxy

when it detects the proxy is not running.

The package Microsoft.AspNetCore.SpaProxy is responsible for the preceding logic to


detect the proxy and redirect the browser.

The hosting startup assembly defined in Properties/launchSettings.json is used to


automatically add the required components during development necessary to detect if
the proxy is running and launch it otherwise:

JSON

{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51783",
"sslPort": 44329
}
},
"profiles": {
"MyReact": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:7145;http://localhost:5273",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES":
"Microsoft.AspNetCore.SpaProxy"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES":
"Microsoft.AspNetCore.SpaProxy"
}
}
}
}

Setup for the client app


This setup is specific to the frontend framework the app is using, however many aspects
of the configuration are similar.

Angular setup
The template generated ClientApp/package.json file:

JSON

{
"name": "myangular",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"prestart": "node aspnetcore-https",
"start": "run-script-os",
"start:windows": "ng serve --port 44483 --ssl --ssl-cert
\"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem\" --ssl-key
\"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.key\"",
"start:default": "ng serve --port 44483 --ssl --ssl-cert
\"$HOME/.aspnet/https/${npm_package_name}.pem\" --ssl-key
\"$HOME/.aspnet/https/${npm_package_name}.key\"",
"build": "ng build",
"build:ssr": "ng run MyAngular:server:dev",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^14.1.3",
"@angular/common": "^14.1.3",
"@angular/compiler": "^14.1.3",
"@angular/core": "^14.1.3",
"@angular/forms": "^14.1.3",
"@angular/platform-browser": "^14.1.3",
"@angular/platform-browser-dynamic": "^14.1.3",
"@angular/platform-server": "^14.1.3",
"@angular/router": "^14.1.3",
"bootstrap": "^5.2.0",
"jquery": "^3.6.0",
"oidc-client": "^1.11.5",
"popper.js": "^1.16.0",
"run-script-os": "^1.1.6",
"rxjs": "~7.5.6",
"tslib": "^2.4.0",
"zone.js": "~0.11.8"
},
"devDependencies": {
"@angular-devkit/build-angular": "^14.1.3",
"@angular/cli": "^14.1.3",
"@angular/compiler-cli": "^14.1.3",
"@types/jasmine": "~4.3.0",
"@types/jasminewd2": "~2.0.10",
"@types/node": "^18.7.11",
"jasmine-core": "~4.3.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.1",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"typescript": "~4.7.4"
},
"overrides": {
"autoprefixer": "10.4.5"
},
"optionalDependencies": {}
}

Contains scripts that launching the angular development server:

The prestart script invokes ClientApp/aspnetcore-https.js , which is responsible


for ensuring the development server HTTPS certificate is available to the SPA proxy
server.

The start:windows and start:default :


Launch the Angular development server via ng serve .
Provide the port, the options to use HTTPS, and the path to the certificate and
the associated key. The provide port number matches the port number
specified in the .csproj file.

The template generated ClientApp/angular.json file contains:


The serve command.

A proxyconfig element in the development configuration to indicate that


proxy.conf.js should be used to configure the frontend proxy, as shown in the

following highlighted JSON:

JSON

{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"MyAngular": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"progress": false,
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"allowedCommonJsDependencies": [
"oidc-client"
],
"assets": [
"src/assets"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "MyAngular:build:production"
},
"development": {
"browserTarget": "MyAngular:build:development",
"proxyConfig": "proxy.conf.js"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "MyAngular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist-server",
"main": "src/main.ts",
"tsConfig": "tsconfig.server.json"
},
"configurations": {
"dev": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": true
},
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false
}
}
}
}
}
},
"defaultProject": "MyAngular"
}

ClientApp/proxy.conf.js defines the routes that need to be proxied back to the server

backend. The general set of options is defined at http-proxy-middleware for react and
angular since they both use the same proxy.

The following highlighted code from ClientApp/proxy.conf.js uses logic based on the
environment variables set during development to determine the port the backend is
running on:

JavaScript

const { env } = require('process');


const target = env.ASPNETCORE_HTTPS_PORT ?
`https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] :
'http://localhost:51951';

const PROXY_CONFIG = [
{
context: [
"/weatherforecast",
],
target: target,
secure: false,
headers: {
Connection: 'Keep-Alive'
}
}
]

module.exports = PROXY_CONFIG;

React setup
The package.json scripts section contains the following scripts that launches the
react app during development, as shown in the following highlighted code:

JSON

{
"name": "myreact",
"version": "0.1.0",
"private": true,
"dependencies": {
"bootstrap": "^5.2.0",
"http-proxy-middleware": "^2.0.6",
"jquery": "^3.6.0",
"merge": "^2.1.1",
"oidc-client": "^1.11.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-bootstrap": "^0.26.2",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"reactstrap": "^9.1.3",
"rimraf": "^3.0.2",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
"workbox-google-analytics": "^6.5.4",
"workbox-navigation-preload": "^6.5.4",
"workbox-precaching": "^6.5.4",
"workbox-range-requests": "^6.5.4",
"workbox-routing": "^6.5.4",
"workbox-strategies": "^6.5.4",
"workbox-streams": "^6.5.4"
},
"devDependencies": {
"ajv": "^8.11.0",
"cross-env": "^7.0.3",
"eslint": "^8.22.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.30.1",
"nan": "^2.16.0",
"typescript": "^4.7.4"
},
"overrides": {
"autoprefixer": "10.4.5"
},
"resolutions": {
"css-what": "^5.0.1",
"nth-check": "^3.0.1"
},
"scripts": {
"prestart": "node aspnetcore-https && node aspnetcore-react",
"start": "rimraf ./build && react-scripts start",
"build": "react-scripts build",
"test": "cross-env CI=true react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint ./src/"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

The prestart script invokes:


aspnetcore-https.js , which is responsible for ensuring the development server

HTTPS certificate is available to the SPA proxy server.


Invokes aspnetcore-react.js to setup the appropriate .env.development.local
file to use the HTTPS local development certificate. aspnetcore-react.js
configures the HTTPS local development certificate by adding SSL_CRT_FILE=
<certificate-path> and SSL_KEY_FILE=<key-path> to the file.

The .env.development file defines the port for the development server and
specifies HTTPS.

The src/setupProxy.js configures the SPA proxy to forward the requests to the
backend. The general set of options is defined in http-proxy-middleware .

The following highlighted code in ClientApp/src/setupProxy.js uses logic based on the


environment variables set during development to determine the port the backend is
running on:

JavaScript

const { createProxyMiddleware } = require('http-proxy-middleware');


const { env } = require('process');

const target = env.ASPNETCORE_HTTPS_PORT ?


`https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] :
'http://localhost:51783';

const context = [
"/weatherforecast",
];

const onError = (err, req, resp, target) => {


console.error(`${err.message}`);
}

module.exports = function (app) {


const appProxy = createProxyMiddleware(context, {
target: target,
// Handle errors to prevent the proxy middleware from crashing when
// the ASP NET Core webserver is unavailable
onError: onError,
secure: false,
// Uncomment this line to add support for proxying websockets
//ws: true,
headers: {
Connection: 'Keep-Alive'
}
});
app.use(appProxy);
};

Supported SPA framework version in ASP.NET


Core SPA templates
The SPA project templates that ship with each ASP.NET Core release reference the latest
version of the appropriate SPA framework.

SPA frameworks typically have a shorter release cycle than .NET. Because of the two
different release cycles, the supported version of the SPA framework and .NET can get
out of sync: the major SPA framework version, that a .NET major release depends on, can
go out of support, while the .NET version the SPA framework shipped with is still
supported.

The ASP.NET Core SPA templates can be updated in a patch release to a new SPA
framework version to keep the templates in a supported and safe state.

Additional resources
Introduction to authentication for Single Page Apps on ASP.NET Core
Use Angular with ASP.NET Core
Use React with ASP.NET Core
Hosting Startup Assemblies
Use the Angular project template with
ASP.NET Core
Article • 12/01/2022

The ASP.NET Core with Angular project template provides a convenient starting point
for ASP.NET Core apps using Angular and the Angular CLI to implement a rich, client-
side user interface (UI).

The project template is equivalent to creating both an ASP.NET Core project to act as a
web API and an Angular CLI project to act as a UI. This project combination offers the
convenience of hosting both projects in a single ASP.NET Core project that can be built
and published as a single unit.

The project template isn't meant for server-side rendering (SSR).

Create a new app


Create a new project from a command prompt using the command dotnet new angular
in an empty directory. For example, the following commands create the app in a my-new-
app directory and switch to that directory:

.NET CLI

dotnet new angular -o my-new-app


cd my-new-app

Run the app from either Visual Studio or the .NET Core CLI:

Visual Studio

Open the generated .csproj file, and run the app as normal from there.

The build process restores npm dependencies on the first run, which can take
several minutes. Subsequent builds are much faster.

The project template creates an ASP.NET Core app and an Angular app. The ASP.NET
Core app is intended to be used for data access, authorization, and other server-side
concerns. The Angular app, residing in the ClientApp subdirectory, is intended to be
used for all UI concerns.
Add pages, images, styles, and modules
The ClientApp directory contains a standard Angular CLI app. See the official Angular
documentation for more information.

There are slight differences between the Angular app created by this template and the
one created by Angular CLI itself (via ng new ); however, the app's capabilities are
unchanged. The app created by the template contains a Bootstrap -based layout and a
basic routing example.

Run ng commands
In a command prompt, switch to the ClientApp subdirectory:

Console

cd ClientApp

If you have the ng tool installed globally, you can run any of its commands. For
example, you can run ng lint , ng test , or any of the other Angular CLI commands .
There's no need to run ng serve though, because your ASP.NET Core app deals with
serving both server-side and client-side parts of your app. Internally, it uses ng serve in
development.

If you don't have the ng tool installed, run npm run ng instead. For example, you can run
npm run ng lint or npm run ng test .

Install npm packages


To install third-party npm packages, use a command prompt in the ClientApp
subdirectory. For example:

Console

cd ClientApp
npm install <package_name>

Publish and deploy


In development, the app runs in a mode optimized for developer convenience. For
example, JavaScript bundles include source maps (so that when debugging, you can see
your original TypeScript code). The app watches for TypeScript, HTML, and CSS file
changes on disk and automatically recompiles and reloads when it sees those files
change.

In production, serve a version of your app that's optimized for performance. This is
configured to happen automatically. When you publish, the build configuration emits a
minified, ahead-of-time (AoT) compiled build of your client-side code. Unlike the
development build, the production build doesn't require Node.js to be installed on the
server (unless you have enabled server-side rendering (SSR)).

You can use standard ASP.NET Core hosting and deployment methods.

Run "ng serve" independently


The project is configured to start its own instance of the Angular CLI server in the
background when the ASP.NET Core app starts in development mode. This is convenient
because you don't have to run a separate server manually.

There's a drawback to this default setup. Each time you modify your C# code and your
ASP.NET Core app needs to restart, the Angular CLI server restarts. Around 10 seconds is
required to start back up. If you're making frequent C# code edits and don't want to
wait for Angular CLI to restart, run the Angular CLI server externally, independently of
the ASP.NET Core process.

To run the Angular CLI server externally, switch to the ClientApp subdirectory in a
command prompt and launch the Angular CLI development server:

Console

cd ClientApp
npm start

When you start your ASP.NET Core app, it won't launch an Angular CLI server. The
instance you started manually is used instead. This enables it to start and restart faster.
It's no longer waiting for Angular CLI to rebuild your client app each time.

When the proxy is launched, the target URL and port is inferred from the environment
variables set by .NET, ASPNETCORE_URLS and ASPNETCORE_HTTPS_PORT . To set the URLs or
HTTPS port, use one of the environment variables or change the value in
proxy.conf.json .
Configure proxy middleware for SignalR
For more information, see http-proxy-middleware .

Additional resources
Introduction to authentication for Single Page Apps on ASP.NET Core
Use React with ASP.NET Core
Article • 06/03/2022

The ASP.NET Core with React project template provides a convenient starting point for
ASP.NET Core apps using React and Create React App (CRA) to implement a rich,
client-side user interface (UI).

The project template is equivalent to creating both an ASP.NET Core project to act as a
web API and a CRA React project to act as a UI. This project combination offers the
convenience of hosting both projects in a single ASP.NET Core project that can be built
and published as a single unit.

The project template isn't meant for server-side rendering (SSR). For SSR with React and
Node.js, consider Next.js or Razzle .

Create a new app


Create a new project from a command prompt using the command dotnet new react in
an empty directory. For example, the following commands create the app in a my-new-
app directory and switch to that directory:

.NET CLI

dotnet new react -o my-new-app


cd my-new-app

Run the app from either Visual Studio or the .NET Core CLI:

Visual Studio

Open the generated .csproj file, and run the app as normal from there.

The build process restores npm dependencies on the first run, which can take
several minutes. Subsequent builds are much faster.

The project template creates an ASP.NET Core app and a React app. The ASP.NET Core
app is intended to be used for data access, authorization, and other server-side
concerns. The React app, residing in the ClientApp subdirectory, is intended to be used
for all UI concerns.
Add pages, images, styles, modules, etc.
The ClientApp directory is a standard CRA React app. See the official CRA
documentation for more information.

There are slight differences between the React app created by this template and the one
created by CRA itself; however, the app's capabilities are unchanged. The app created by
the template contains a Bootstrap -based layout and a basic routing example.

Install npm packages


To install third-party npm packages, use a command prompt in the ClientApp
subdirectory. For example:

Console

cd ClientApp
npm install <package_name>

Publish and deploy


In development, the app runs in a mode optimized for developer convenience. For
example, JavaScript bundles include source maps (so that when debugging, you can see
your original source code). The app watches JavaScript, HTML, and CSS file changes on
disk and automatically recompiles and reloads when it sees those files change.

In production, serve a version of your app that's optimized for performance. This is
configured to happen automatically. When you publish, the build configuration emits a
minified, transpiled build of your client-side code. Unlike the development build, the
production build doesn't require Node.js to be installed on the server.

You can use standard ASP.NET Core hosting and deployment methods.

Run the CRA server independently


The project is configured to start its own instance of the CRA development server in the
background when the ASP.NET Core app starts in development mode. This is convenient
because it means you don't have to run a separate server manually.

There's a drawback to this default setup. Each time you modify your C# code and your
ASP.NET Core app needs to restart, the CRA server restarts. A few seconds are required
to start back up. If you're making frequent C# code edits and don't want to wait for the
CRA server to restart, run the CRA server externally, independently of the ASP.NET Core
process.

To run the CRA server externally, switch to the ClientApp subdirectory in a command
prompt and launch the CRA development server:

Console

cd ClientApp
npm start

When you start your ASP.NET Core app, it won't launch a CRA server. The instance you
started manually is used instead. This enables it to start and restart faster. It's no longer
waiting for your React app to rebuild each time.

Configure proxy middleware for SignalR


For more information, see http-proxy-middleware .

Additional resources
Introduction to authentication for Single Page Apps on ASP.NET Core
Use JavaScript Services to Create Single
Page Applications in ASP.NET Core
Article • 12/03/2022

By Fiyaz Hasan

2 Warning

The features described in this article are obsolete as of ASP.NET Core 3.0. A simpler
SPA frameworks integration mechanism is available in the
Microsoft.AspNetCore.SpaServices.Extensions NuGet package. For more
information, see [Announcement] Obsoleting Microsoft.AspNetCore.SpaServices
and Microsoft.AspNetCore.NodeServices .

A Single Page Application (SPA) is a popular type of web application due to its inherent
rich user experience. Integrating client-side SPA frameworks or libraries, such as
Angular or React , with server-side frameworks such as ASP.NET Core can be
difficult. JavaScript Services was developed to reduce friction in the integration process.
It enables seamless operation between the different client and server technology stacks.

What is JavaScript Services


JavaScript Services is a collection of client-side technologies for ASP.NET Core. Its goal is
to position ASP.NET Core as developers' preferred server-side platform for building
SPAs.

JavaScript Services consists of two distinct NuGet packages:

Microsoft.AspNetCore.NodeServices (NodeServices)
Microsoft.AspNetCore.SpaServices (SpaServices)

These packages are useful in the following scenarios:

Run JavaScript on the server


Use a SPA framework or library
Build client-side assets with Webpack

Much of the focus in this article is placed on using the SpaServices package.
What is SpaServices
SpaServices was created to position ASP.NET Core as developers' preferred server-side
platform for building SPAs. SpaServices isn't required to develop SPAs with ASP.NET
Core, and it doesn't lock developers into a particular client framework.

SpaServices provides useful infrastructure such as:

Server-side prerendering
Webpack Dev Middleware
Hot Module Replacement
Routing helpers

Collectively, these infrastructure components enhance both the development workflow


and the runtime experience. The components can be adopted individually.

Prerequisites for using SpaServices


To work with SpaServices, install the following:

Node.js (version 6 or later) with npm

To verify these components are installed and can be found, run the following
from the command line:

Console

node -v && npm -v

If deploying to an Azure web site, no action is required—Node.js is installed and


available in the server environments.

.NET Core SDK 2.0 or later


On Windows using Visual Studio 2017, the SDK is installed by selecting the .NET
Core cross-platform development workload.

Microsoft.AspNetCore.SpaServices NuGet package

Server-side prerendering
A universal (also known as isomorphic) application is a JavaScript application capable of
running both on the server and the client. Angular, React, and other popular frameworks
provide a universal platform for this application development style. The idea is to first
render the framework components on the server via Node.js, and then delegate further
execution to the client.

ASP.NET Core Tag Helpers provided by SpaServices simplify the implementation of


server-side prerendering by invoking the JavaScript functions on the server.

Server-side prerendering prerequisites


Install the aspnet-prerendering npm package:

Console

npm i -S aspnet-prerendering

Server-side prerendering configuration


The Tag Helpers are made discoverable via namespace registration in the project's
_ViewImports.cshtml file:

CSHTML

@using SpaServicesSampleApp
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"

These Tag Helpers abstract away the intricacies of communicating directly with low-level
APIs by leveraging an HTML-like syntax inside the Razor view:

CSHTML

<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>

asp-prerender-module Tag Helper


The asp-prerender-module Tag Helper, used in the preceding code example, executes
ClientApp/dist/main-server.js on the server via Node.js. For clarity's sake, main-

server.js file is an artifact of the TypeScript-to-JavaScript transpilation task in the

Webpack build process. Webpack defines an entry point alias of main-server ; and,
traversal of the dependency graph for this alias begins at the ClientApp/boot-server.ts
file:
JavaScript

entry: { 'main-server': './ClientApp/boot-server.ts' },

In the following Angular example, the ClientApp/boot-server.ts file utilizes the


createServerRenderer function and RenderResult type of the aspnet-prerendering npm

package to configure server rendering via Node.js. The HTML markup destined for
server-side rendering is passed to a resolve function call, which is wrapped in a strongly-
typed JavaScript Promise object. The Promise object's significance is that it
asynchronously supplies the HTML markup to the page for injection in the DOM's
placeholder element.

TypeScript

import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {


const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url:
params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];

return
platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef
=> {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);

return new Promise<RenderResult>((resolve, reject) => {


zone.onError.subscribe(errorInfo => reject(errorInfo));
appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to
delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: state.renderToString()
});
moduleRef.destroy();
});
});
});
});
});

asp-prerender-data Tag Helper


When coupled with the asp-prerender-module Tag Helper, the asp-prerender-data Tag
Helper can be used to pass contextual information from the Razor view to the server-
side JavaScript. For example, the following markup passes user data to the main-server
module:

CSHTML

<app asp-prerender-module="ClientApp/dist/main-server"
asp-prerender-data='new {
UserName = "John Doe"
}'>Loading...</app>

The received UserName argument is serialized using the built-in JSON serializer and is
stored in the params.data object. In the following Angular example, the data is used to
construct a personalized greeting within an h1 element:

TypeScript

import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {


const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url:
params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];

return
platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef
=> {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);

return new Promise<RenderResult>((resolve, reject) => {


const result = `<h1>Hello, ${params.data.userName}</h1>`;

zone.onError.subscribe(errorInfo => reject(errorInfo));


appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to
delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: result
});
moduleRef.destroy();
});
});
});
});
});

Property names passed in Tag Helpers are represented with PascalCase notation.
Contrast that to JavaScript, where the same property names are represented with
camelCase. The default JSON serialization configuration is responsible for this
difference.

To expand upon the preceding code example, data can be passed from the server to the
view by hydrating the globals property provided to the resolve function:

TypeScript

import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {


const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url:
params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];

return
platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef
=> {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);

return new Promise<RenderResult>((resolve, reject) => {


const result = `<h1>Hello, ${params.data.userName}</h1>`;

zone.onError.subscribe(errorInfo => reject(errorInfo));


appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to
delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: result,
globals: {
postList: [
'Introduction to ASP.NET Core',
'Making apps with Angular and ASP.NET Core'
]
}
});
moduleRef.destroy();
});
});
});
});
});

The postList array defined inside the globals object is attached to the browser's global
window object. This variable hoisting to global scope eliminates duplication of effort,
particularly as it pertains to loading the same data once on the server and again on the
client.

Webpack Dev Middleware


Webpack Dev Middleware introduces a streamlined development workflow whereby
Webpack builds resources on demand. The middleware automatically compiles and
serves client-side resources when a page is reloaded in the browser. The alternate
approach is to manually invoke Webpack via the project's npm build script when a third-
party dependency or the custom code changes. An npm build script in the package.json
file is shown in the following example:

JSON

"build": "npm run build:vendor && npm run build:custom",

Webpack Dev Middleware prerequisites


Install the aspnet-webpack npm package:

Console

npm i -D aspnet-webpack
Webpack Dev Middleware configuration
Webpack Dev Middleware is registered into the HTTP request pipeline via the following
code in the Startup.cs file's Configure method:

C#

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

// Call UseWebpackDevMiddleware before UseStaticFiles


app.UseStaticFiles();

The UseWebpackDevMiddleware extension method must be called before registering static


file hosting via the UseStaticFiles extension method. For security reasons, register the
middleware only when the app runs in development mode.

The webpack.config.js file's output.publicPath property tells the middleware to watch


the dist folder for changes:

JavaScript

module.exports = (env) => {


output: {
filename: '[name].js',
publicPath: '/dist/' // Webpack dev middleware, if enabled,
handles requests for this URL prefix
},

Hot Module Replacement


Think of Webpack's Hot Module Replacement (HMR) feature as an evolution of
Webpack Dev Middleware. HMR introduces all the same benefits, but it further
streamlines the development workflow by automatically updating page content after
compiling the changes. Don't confuse this with a refresh of the browser, which would
interfere with the current in-memory state and debugging session of the SPA. There's a
live link between the Webpack Dev Middleware service and the browser, which means
changes are pushed to the browser.
Hot Module Replacement prerequisites
Install the webpack-hot-middleware npm package:

Console

npm i -D webpack-hot-middleware

Hot Module Replacement configuration


The HMR component must be registered into MVC's HTTP request pipeline in the
Configure method:

C#

app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true
});

As was true with Webpack Dev Middleware, the UseWebpackDevMiddleware extension


method must be called before the UseStaticFiles extension method. For security
reasons, register the middleware only when the app runs in development mode.

The webpack.config.js file must define a plugins array, even if it's left empty:

JavaScript

module.exports = (env) => {


plugins: [new CheckerPlugin()]

After loading the app in the browser, the developer tools' Console tab provides
confirmation of HMR activation:
Routing helpers
In most ASP.NET Core-based SPAs, client-side routing is often desired in addition to
server-side routing. The SPA and MVC routing systems can work independently without
interference. There's, however, one edge case posing challenges: identifying 404 HTTP
responses.

Consider the scenario in which an extensionless route of /some/page is used. Assume the
request doesn't pattern-match a server-side route, but its pattern does match a client-
side route. Now consider an incoming request for /images/user-512.png , which
generally expects to find an image file on the server. If that requested resource path
doesn't match any server-side route or static file, it's unlikely that the client-side
application would handle it—generally returning a 404 HTTP status code is desired.

Routing helpers prerequisites


Install the client-side routing npm package. Using Angular as an example:

Console

npm i -S @angular/router

Routing helpers configuration


An extension method named MapSpaFallbackRoute is used in the Configure method:
C#

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");

routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});

Routes are evaluated in the order in which they're configured. Consequently, the
default route in the preceding code example is used first for pattern matching.

Create a new project


JavaScript Services provide pre-configured application templates. SpaServices is used in
these templates in conjunction with different frameworks and libraries such as Angular,
React, and Redux.

These templates can be installed via the .NET Core CLI by running the following
command:

.NET CLI

dotnet new --install Microsoft.AspNetCore.SpaTemplates::*

A list of available SPA templates is displayed:

Templates Short Name Language Tags

MVC ASP.NET Core with Angular angular [C#] Web/MVC/SPA

MVC ASP.NET Core with React.js react [C#] Web/MVC/SPA

MVC ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA

To create a new project using one of the SPA templates, include the Short Name of the
template in the dotnet new command. The following command creates an Angular
application with ASP.NET Core MVC configured for the server side:

.NET CLI

dotnet new angular


Set the runtime configuration mode
Two primary runtime configuration modes exist:

Development:
Includes source maps to ease debugging.
Doesn't optimize the client-side code for performance.
Production:
Excludes source maps.
Optimizes the client-side code via bundling and minification.

ASP.NET Core uses an environment variable named ASPNETCORE_ENVIRONMENT to store the


configuration mode. For more information, see Set the environment.

Run with .NET Core CLI


Restore the required NuGet and npm packages by running the following command at
the project root:

.NET CLI

dotnet restore && npm i

Build and run the application:

.NET CLI

dotnet run

The application starts on localhost according to the runtime configuration mode.


Navigating to http://localhost:5000 in the browser displays the landing page.

Run with Visual Studio 2017


Open the .csproj file generated by the dotnet new command. The required NuGet and
npm packages are restored automatically upon project open. This restoration process
may take up to a few minutes, and the application is ready to run when it completes.
Click the green run button or press Ctrl + F5 , and the browser opens to the
application's landing page. The application runs on localhost according to the runtime
configuration mode.
Test the app
SpaServices templates are pre-configured to run client-side tests using Karma and
Jasmine . Jasmine is a popular unit testing framework for JavaScript, whereas Karma is
a test runner for those tests. Karma is configured to work with the Webpack Dev
Middleware such that the developer isn't required to stop and run the test every time
changes are made. Whether it's the code running against the test case or the test case
itself, the test runs automatically.

Using the Angular application as an example, two Jasmine test cases are already
provided for the CounterComponent in the counter.component.spec.ts file:

TypeScript

it('should display a title', async(() => {


const titleText = fixture.nativeElement.querySelector('h1').textContent;
expect(titleText).toEqual('Counter');
}));

it('should start with count 0, then increments by 1 when clicked', async(()


=> {
const countElement = fixture.nativeElement.querySelector('strong');
expect(countElement.textContent).toEqual('0');

const incrementButton = fixture.nativeElement.querySelector('button');


incrementButton.click();
fixture.detectChanges();
expect(countElement.textContent).toEqual('1');
}));

Open the command prompt in the ClientApp directory. Run the following command:

Console

npm test

The script launches the Karma test runner, which reads the settings defined in the
karma.conf.js file. Among other settings, the karma.conf.js identifies the test files to

be executed via its files array:

JavaScript

module.exports = function (config) {


config.set({
files: [
'../../wwwroot/dist/vendor.js',
'./boot-tests.ts'
],

Publish the app


See this GitHub issue for more information on publishing to Azure.

Combining the generated client-side assets and the published ASP.NET Core artifacts
into a ready-to-deploy package can be cumbersome. Thankfully, SpaServices
orchestrates that entire publication process with a custom MSBuild target named
RunWebpack :

XML

<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">


<!-- As part of publishing, ensure the JS resources are freshly built in
production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config
webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />

<!-- Include the newly-built files in the publish output -->


<ItemGroup>
<DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')"
Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>

The MSBuild target has the following responsibilities:

1. Restore the npm packages.


2. Create a production-grade build of the third-party, client-side assets.
3. Create a production-grade build of the custom client-side assets.
4. Copy the Webpack-generated assets to the publish folder.

The MSBuild target is invoked when running:

.NET CLI

dotnet publish -c Release


Additional resources
Angular Docs
Client-side library acquisition in ASP.NET
Core with LibMan
Article • 06/03/2022

By Scott Addie

Library Manager (LibMan) is a lightweight, client-side library acquisition tool. LibMan


downloads popular libraries and frameworks from the file system or from a content
delivery network (CDN) . The supported CDNs include CDNJS , jsDelivr , and
unpkg . The selected library files are fetched and placed in the appropriate location
within the ASP.NET Core project.

LibMan use cases


LibMan offers the following benefits:

Only the library files you need are downloaded.


Additional tooling, such as Node.js , npm , and WebPack , isn't necessary to
acquire a subset of files in a library.
Files can be placed in a specific location without resorting to build tasks or manual
file copying.

LibMan isn't a package management system. If you're already using a package manager,
such as npm or yarn , continue doing so. LibMan wasn't developed to replace those
tools.

Additional resources
Use LibMan with ASP.NET Core in Visual Studio
Use the LibMan CLI with ASP.NET Core
LibMan GitHub repository
Use the LibMan CLI with ASP.NET Core
Article • 06/03/2022

By Scott Addie

The LibMan CLI is a cross-platform tool that's supported everywhere .NET Core is
supported.

Prerequisites
.NET Core 2.1 SDK or later

Installation
To install the LibMan CLI:

.NET CLI

dotnet tool install -g Microsoft.Web.LibraryManager.Cli

A .NET Core Global Tool is installed from the Microsoft.Web.LibraryManager.Cli NuGet


package.

To install the LibMan CLI from a specific NuGet package source:

.NET CLI

dotnet tool install -g Microsoft.Web.LibraryManager.Cli --version 1.0.94-


g606058a278 --add-source C:\Temp\

In the preceding example, a .NET Core Global Tool is installed from the local Windows
machine's C:\Temp\Microsoft.Web.LibraryManager.Cli.1.0.94-g606058a278.nupkg file.

Usage
After successful installation of the CLI, the following command can be used:

Console

libman
To view the installed CLI version:

Console

libman --version

To view the available CLI commands:

Console

libman --help

The preceding command displays output similar to the following:

Console

1.0.163+g45474d37ed

Usage: libman [options] [command]

Options:
--help|-h Show help information
--version Show version information

Commands:
cache List or clean libman cache contents
clean Deletes all library files defined in libman.json from the
project
init Create a new libman.json
install Add a library definition to the libman.json file, and download
the
library to the specified location
restore Downloads all files from provider and saves them to specified
destination
uninstall Deletes all files for the specified library from their
specified
destination, then removes the specified library definition from
libman.json
update Updates the specified library

Use "libman [command] --help" for more information about a command.

The following sections outline the available CLI commands.

Initialize LibMan in the project


The libman init command creates a libman.json file if one doesn't exist. The file is
created with the default item template content.

Synopsis
Console

libman init [-d|--default-destination] [-p|--default-provider] [--verbosity]


libman init [-h|--help]

Options
The following options are available for the libman init command:

-d|--default-destination <PATH>

A path relative to the current folder. Library files are installed in this location if no
destination property is defined for a library in libman.json . The <PATH> value is
written to the defaultDestination property of libman.json .

-p|--default-provider <PROVIDER>

The provider to use if no provider is defined for a given library. The <PROVIDER>
value is written to the defaultProvider property of libman.json . Replace
<PROVIDER> with one of the following values:
cdnjs

filesystem
jsdelivr

unpkg

-h|--help

Show help information.

--verbosity <LEVEL>

Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet

normal
detailed
Examples
To create a libman.json file in an ASP.NET Core project:

Navigate to the project root.

Run the following command:

Console

libman init

Type the name of the default provider, or press Enter to use the default CDNJS
provider. Valid values include:
cdnjs
filesystem

jsdelivr

unpkg

A libman.json file is added to the project root with the following content:

JSON

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": []
}

Add library files


The libman install command downloads and installs library files into the project. A
libman.json file is added if one doesn't exist. The libman.json file is modified to store
configuration details for the library files.

Synopsis
Console

libman install <LIBRARY> [-d|--destination] [--files] [-p|--provider] [--


verbosity]
libman install [-h|--help]

Arguments
LIBRARY

The name of the library to install. This name may include version number notation (for
example, @1.2.0 ).

Options
The following options are available for the libman install command:

-d|--destination <PATH>

The location to install the library. If not specified, the default location is used. If no
defaultDestination property is specified in libman.json , this option is required.

--files <FILE>

Specify the name of the file to install from the library. If not specified, all files from
the library are installed. Provide one --files option per file to be installed.
Relative paths are supported too. For example: --files dist/browser/signalr.js .

-p|--provider <PROVIDER>

The name of the provider to use for the library acquisition. Replace <PROVIDER>
with one of the following values:
cdnjs
filesystem

jsdelivr
unpkg
If not specified, the defaultProvider property in libman.json is used. If no
defaultProvider property is specified in libman.json , this option is required.

-h|--help

Show help information.

--verbosity <LEVEL>

Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal

detailed

Examples
Consider the following libman.json file:

JSON

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": []
}

To install the jQuery version 3.2.1 jquery.min.js file to the wwwroot/scripts/jquery folder
using the CDNJS provider:

Console

libman install jquery@3.2.1 --provider cdnjs --destination


wwwroot/scripts/jquery --files jquery.min.js

The libman.json file resembles the following:

JSON

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.2.1",
"destination": "wwwroot/scripts/jquery",
"files": [
"jquery.min.js"
]
}
]
}

To install the calendar.js and calendar.css files from C:\temp\contosoCalendar\ using


the file system provider:

Console

libman install C:\temp\contosoCalendar\ --provider filesystem --files


calendar.js --files calendar.css

The following prompt appears for two reasons:

The libman.json file doesn't contain a defaultDestination property.


The libman install command doesn't contain the -d|--destination option.

After accepting the default destination, the libman.json file resembles the following:

JSON

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.2.1",
"destination": "wwwroot/scripts/jquery",
"files": [
"jquery.min.js"
]
},
{
"library": "C:\\temp\\contosoCalendar\\",
"provider": "filesystem",
"destination": "wwwroot/lib/contosoCalendar",
"files": [
"calendar.js",
"calendar.css"
]
}
]
}

Restore library files


The libman restore command installs library files defined in libman.json . The following
rules apply:

If no libman.json file exists in the project root, an error is returned.


If a library specifies a provider, the defaultProvider property in libman.json is
ignored.
If a library specifies a destination, the defaultDestination property in libman.json
is ignored.

Synopsis
Console

libman restore [--verbosity]


libman restore [-h|--help]

Options
The following options are available for the libman restore command:

-h|--help

Show help information.

--verbosity <LEVEL>

Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet

normal
detailed
Examples
To restore the library files defined in libman.json :

Console

libman restore

Delete library files


The libman clean command deletes library files previously restored via LibMan. Folders
that become empty after this operation are deleted. The library files' associated
configurations in the libraries property of libman.json aren't removed.

Synopsis
Console

libman clean [--verbosity]


libman clean [-h|--help]

Options
The following options are available for the libman clean command:

-h|--help

Show help information.

--verbosity <LEVEL>

Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet

normal

detailed

Examples
To delete library files installed via LibMan:

Console
libman clean

Uninstall library files


The libman uninstall command:

Deletes all files associated with the specified library from the destination in
libman.json .

Removes the associated library configuration from libman.json .

An error occurs when:

No libman.json file exists in the project root.


The specified library doesn't exist.

If more than one library with the same name is installed, you're prompted to choose
one.

Synopsis
Console

libman uninstall <LIBRARY> [--verbosity]


libman uninstall [-h|--help]

Arguments
LIBRARY

The name of the library to uninstall. This name may include version number notation (for
example, @1.2.0 ).

Options
The following options are available for the libman uninstall command:

-h|--help

Show help information.

--verbosity <LEVEL>
Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal

detailed

Examples
Consider the following libman.json file:

JSON

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.3.1",
"files": [
"jquery.min.js",
"jquery.js",
"jquery.min.map"
],
"destination": "wwwroot/lib/jquery/"
},
{
"provider": "unpkg",
"library": "bootstrap@4.1.3",
"destination": "wwwroot/lib/bootstrap/"
},
{
"provider": "filesystem",
"library": "C:\\temp\\lodash\\",
"files": [
"lodash.js",
"lodash.min.js"
],
"destination": "wwwroot/lib/lodash/"
}
]
}

To uninstall jQuery, either of the following commands succeed:

Console

libman uninstall jquery

Console
libman uninstall jquery@3.3.1

To uninstall the Lodash files installed via the filesystem provider:

Console

libman uninstall C:\temp\lodash\

Update library version


The libman update command updates a library installed via LibMan to the specified
version.

An error occurs when:

No libman.json file exists in the project root.


The specified library doesn't exist.

If more than one library with the same name is installed, you're prompted to choose
one.

Synopsis
Console

libman update <LIBRARY> [-pre] [--to] [--verbosity]


libman update [-h|--help]

Arguments
LIBRARY

The name of the library to update.

Options
The following options are available for the libman update command:

-pre

Obtain the latest prerelease version of the library.


--to <VERSION>

Obtain a specific version of the library.

-h|--help

Show help information.

--verbosity <LEVEL>

Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal

detailed

Examples
To update jQuery to the latest version:

Console

libman update jquery

To update jQuery to version 3.3.1:

Console

libman update jquery --to 3.3.1

To update jQuery to the latest prerelease version:

Console

libman update jquery -pre

Manage library cache


The libman cache command manages the LibMan library cache. The filesystem
provider doesn't use the library cache.

Synopsis
Console

libman cache clean [<PROVIDER>] [--verbosity]


libman cache list [--files] [--libraries] [--verbosity]
libman cache [-h|--help]

Arguments
PROVIDER

Only used with the clean command. Specifies the provider cache to clean. Valid values
include:

cdnjs

filesystem
jsdelivr

unpkg

Options
The following options are available for the libman cache command:

--files

List the names of files that are cached.

--libraries

List the names of libraries that are cached.

-h|--help

Show help information.

--verbosity <LEVEL>

Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal

detailed

Examples
To view the names of cached libraries per provider, use one of the following
commands:

Console

libman cache list

Console

libman cache list --libraries

Output similar to the following is displayed:

Console

Cache contents:
---------------
unpkg:
knockout
react
vue
cdnjs:
font-awesome
jquery
knockout
lodash.js
react

To view the names of cached library files per provider:

Console

libman cache list --files

Output similar to the following is displayed:

Console

Cache contents:
---------------
unpkg:
knockout:
<list omitted for brevity>
react:
<list omitted for brevity>
vue:
<list omitted for brevity>
cdnjs:
font-awesome
metadata.json
jquery
metadata.json
3.2.1\core.js
3.2.1\jquery.js
3.2.1\jquery.min.js
3.2.1\jquery.min.map
3.2.1\jquery.slim.js
3.2.1\jquery.slim.min.js
3.2.1\jquery.slim.min.map
3.3.1\core.js
3.3.1\jquery.js
3.3.1\jquery.min.js
3.3.1\jquery.min.map
3.3.1\jquery.slim.js
3.3.1\jquery.slim.min.js
3.3.1\jquery.slim.min.map
knockout
metadata.json
3.4.2\knockout-debug.js
3.4.2\knockout-min.js
lodash.js
metadata.json
4.17.10\lodash.js
4.17.10\lodash.min.js
react
metadata.json

Notice the preceding output shows that jQuery versions 3.2.1 and 3.3.1 are cached
under the CDNJS provider.

To empty the library cache for the CDNJS provider:

Console

libman cache clean cdnjs

After emptying the CDNJS provider cache, the libman cache list command
displays the following:

Console

Cache contents:
---------------
unpkg:
knockout
react
vue
cdnjs:
(empty)
To empty the cache for all supported providers:

Console

libman cache clean

After emptying all provider caches, the libman cache list command displays the
following:

Console

Cache contents:
---------------
unpkg:
(empty)
cdnjs:
(empty)

Additional resources
Install a Global Tool
Use LibMan with ASP.NET Core in Visual Studio
LibMan GitHub repository
Use LibMan with ASP.NET Core in Visual
Studio
Article • 09/21/2022

By Scott Addie

Visual Studio has built-in support for LibMan in ASP.NET Core projects, including:

Support for configuring and running LibMan restore operations on build.


Menu items for triggering LibMan restore and clean operations.
Search dialog for finding libraries and adding the files to a project.
Editing support for libman.json —the LibMan manifest file.

View or download sample code (how to download)

Prerequisites
Visual Studio 2019 with the ASP.NET and web development workload

Add library files


Library files can be added to an ASP.NET Core project in two different ways:

1. Use the Add Client-Side Library dialog


2. Manually configure LibMan manifest file entries

Use the Add Client-Side Library dialog


Follow these steps to install a client-side library:

In Solution Explorer, right-click the project folder in which the files should be
added. Choose Add > Client-Side Library. The Add Client-Side Library dialog
appears:
Select the library provider from the Provider drop down. CDNJS is the default
provider.

Type the library name to fetch in the Library text box. IntelliSense provides a list of
libraries beginning with the provided text.

Select the library from the IntelliSense list. Notice the library name is suffixed with
the @ symbol and the latest stable version known to the selected provider.

Decide which files to include:


Select the Include all library files radio button to include all of the library's files.
Select the Choose specific files radio button to include a subset of the library's
files. When the radio button is selected, the file selector tree is enabled. Check
the boxes to the left of the file names to download.

Specify the project folder for storing the files in the Target Location text box. As a
recommendation, store each library in a separate folder.

The suggested Target Location folder is based on the location from which the
dialog launched:
If launched from the project root:
wwwroot/lib is used if wwwroot exists.
lib is used if wwwroot doesn't exist.
If launched from a project folder, the corresponding folder name is used.

The folder suggestion is suffixed with the library name. The following table
illustrates folder suggestions when installing jQuery in a Razor Pages project.
Launch location Suggested folder

project root (if wwwroot exists) wwwroot/lib/jquery/

project root (if wwwroot doesn't exist) lib/jquery/

Pages folder in project Pages/jquery/

Click the Install button to download the files, per the configuration in libman.json .

Review the Library Manager feed of the Output window for installation details. For
example:

Console

Restore operation started...


Restoring libraries for project LibManSample
Restoring library jquery@3.3.1... (LibManSample)
wwwroot/lib/jquery/jquery.min.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.min.map written to destination (LibManSample)
Restore operation completed
1 libraries restored in 2.32 seconds

Manually configure LibMan manifest file entries


All LibMan operations in Visual Studio are based on the content of the project root's
LibMan manifest ( libman.json ). You can manually edit libman.json to configure library
files for the project. Visual Studio restores all library files once libman.json is saved.

To open libman.json for editing, the following options exist:

Double-click the libman.json file in Solution Explorer.


Right-click the project in Solution Explorer and select Manage Client-Side
Libraries. †
Select Manage Client-Side Libraries from the Visual Studio Project menu. †

† If the libman.json file doesn't already exist in the project root, it will be created with
the default item template content.

Visual Studio offers rich JSON editing support such as colorization, formatting,
IntelliSense, and schema validation. The LibMan manifest's JSON schema is found at
https://json.schemastore.org/libman .
With the following manifest file, LibMan retrieves files per the configuration defined in
the libraries property. An explanation of the object literals defined within libraries
follows:

A subset of jQuery version 3.3.1 is retrieved from the CDNJS provider. The subset
is defined in the files property— jquery.min.js , jquery.js , and jquery.min.map.
The files are placed in the project's wwwroot/lib/jquery folder.
The entirety of Bootstrap version 4.1.3 is retrieved and placed in a
wwwroot/lib/bootstrap folder. The object literal's provider property overrides the
defaultProvider property value. LibMan retrieves the Bootstrap files from the
unpkg provider.
A subset of Lodash was approved by a governing body within the organization.
The lodash.js and lodash.min.js files are retrieved from the local file system at
C:\temp\lodash\. The files are copied to the project's wwwroot/lib/lodash folder.

JSON

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.3.1",
"files": [
"jquery.min.js",
"jquery.js",
"jquery.min.map"
],
"destination": "wwwroot/lib/jquery/"
},
{
"provider": "unpkg",
"library": "bootstrap@4.1.3",
"destination": "wwwroot/lib/bootstrap/"
},
{
"provider": "filesystem",
"library": "C:\\temp\\lodash\\",
"files": [
"lodash.js",
"lodash.min.js"
],
"destination": "wwwroot/lib/lodash/"
}
]
}
7 Note

LibMan only supports one version of each library from each provider. The
libman.json file fails schema validation if it contains two libraries with the same

library name for a given provider.

Restore library files


To restore library files from within Visual Studio, there must be a valid libman.json file in
the project root. Restored files are placed in the project at the location specified for each
library.

Library files can be restored in an ASP.NET Core project in two ways:

1. Restore files during build


2. Restore files manually

Restore files during build


LibMan can restore the defined library files as part of the build process. By default, the
restore-on-build behavior is disabled.

To enable and test the restore-on-build behavior:

Right-click libman.json in Solution Explorer and select Enable Restore Client-Side


Libraries on Build from the context menu.

Click the Yes button when prompted to install a NuGet package. The
Microsoft.Web.LibraryManager.Build NuGet package is added to the project:

XML

<PackageReference Include="Microsoft.Web.LibraryManager.Build"
Version="1.0.113" />

Build the project to confirm LibMan file restoration occurs. The


Microsoft.Web.LibraryManager.Build package injects an MSBuild target that runs

LibMan during the project's build operation.

Review the Build feed of the Output window for a LibMan activity log:

Console
1>------ Build started: Project: LibManSample, Configuration: Debug Any
CPU ------
1>
1>Restore operation started...
1>Restoring library jquery@3.3.1...
1>Restoring library bootstrap@4.1.3...
1>
1>2 libraries restored in 10.66 seconds
1>LibManSample ->
C:\LibManSample\bin\Debug\netcoreapp2.1\LibManSample.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped
==========

When the restore-on-build behavior is enabled, the libman.json context menu displays
a Disable Restore Client-Side Libraries on Build option. Selecting this option removes
the Microsoft.Web.LibraryManager.Build package reference from the project file.
Consequently, the client-side libraries are no longer restored on each build.

Regardless of the restore-on-build setting, you can manually restore at any time from
the libman.json context menu. For more information, see Restore files manually.

Restore files manually


To manually restore library files:

For all projects in the solution:


Right-click the solution name in Solution Explorer.
Select the Restore Client-Side Libraries option.
For a specific project:
Right-click the libman.json file in Solution Explorer.
Select the Restore Client-Side Libraries option.

While the restore operation is running:

The Task Status Center (TSC) icon on the Visual Studio status bar will be animated
and will read Restore operation started. Clicking the icon opens a tooltip listing the
known background tasks.

Messages will be sent to the status bar and the Library Manager feed of the
Output window. For example:

Console

Restore operation started...


Restoring libraries for project LibManSample
Restoring library jquery@3.3.1... (LibManSample)
wwwroot/lib/jquery/jquery.min.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.min.map written to destination (LibManSample)
Restore operation completed
1 libraries restored in 2.32 seconds

Delete library files


To perform the clean operation, which deletes library files previously restored in Visual
Studio:

Right-click the libman.json file in Solution Explorer.


Select the Clean Client-Side Libraries option.

To prevent unintentional removal of non-library files, the clean operation doesn't delete
whole directories. It only removes files that were included in the previous restore.

While the clean operation is running:

The TSC icon on the Visual Studio status bar will be animated and will read Client
libraries operation started. Clicking the icon opens a tooltip listing the known
background tasks.
Messages are sent to the status bar and the Library Manager feed of the Output
window. For example:

Console

Clean libraries operation started...


Clean libraries operation completed
2 libraries were successfully deleted in 1.91 secs

The clean operation only deletes files from the project. Library files stay in the cache for
faster retrieval on future restore operations. To manage library files stored in the local
machine's cache, use the LibMan CLI.

Uninstall library files


To uninstall library files:

Open libman.json .

Position the caret inside the corresponding libraries object literal.


Click the light bulb icon that appears in the left margin, and select Uninstall
<library_name>@<library_version>:

Alternatively, you can manually edit and save the LibMan manifest ( libman.json ). The
restore operation runs when the file is saved. Library files that are no longer defined in
libman.json are removed from the project.

Update library version


To check for an updated library version:

Open libman.json .
Position the caret inside the corresponding libraries object literal.
Click the light bulb icon that appears in the left margin. Hover over Check for
updates.

LibMan checks for a library version newer than the version installed. The following
outcomes can occur:

A No updates found message is displayed if the latest version is already installed.

The latest stable version is displayed if not already installed.

If a pre-release newer than the installed version is available, the pre-release is


displayed.
To downgrade to an older library version, manually edit the libman.json file. When the
file is saved, the LibMan restore operation:

Removes redundant files from the previous version.


Adds new and updated files from the new version.

Additional resources
Use the LibMan CLI with ASP.NET Core
LibMan GitHub repository
Run .NET from JavaScript
Article • 11/10/2022

This article explains how to run .NET from JavaScript (JS) using JS
[JSImport] / [JSExport] interop.

Existing JS apps can use the expanded client-side WebAssembly support in .NET 7 to
reuse .NET libraries from JS or to build novel .NET-based apps and frameworks.

7 Note

This article focuses on running .NET from JS apps without any dependency on
Blazor. For guidance on using [JSImport] / [JSExport] interop in Blazor
WebAssembly apps, see JavaScript JSImport/JSExport interop with ASP.NET Core
Blazor WebAssembly.

These approaches are appropriate when you only expect to run on WebAssembly
(WASM). Libraries can make a runtime check to determine if the app is running on
WASM by calling OperatingSystem.IsBrowser.

Prerequisites
.NET 7.0 SDK

Install the latest version of the .NET SDK .

Install the wasm-tools workload, which brings in the related MSBuild targets.

.NET CLI

dotnet workload install wasm-tools

Optionally, install the wasm-experimental workload, which contains experimental project


templates for getting started with .NET on WebAssembly in a browser app
(WebAssembly Browser App) or in a Node.js-based console app (WebAssembly Console
App). This workload isn't required if you plan to integrate JS [JSImport] / [JSExport]
interop into an existing JS app.

.NET CLI

dotnet workload install wasm-experimental


For more information, see the Experimental workload and project templates section.

Namespace
The JS interop API described in this article is controlled by attributes in the
System.Runtime.InteropServices.JavaScript namespace.

Project configuration
To configure a project ( .csproj ) to enable JS interop:

Target net7.0 or later:

XML

<TargetFramework>net7.0</TargetFramework>

Specify browser-wasm for the runtime identifier:

XML

<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>

Specify an executable output type:

XML

<OutputType>Exe</OutputType>

Enable the AllowUnsafeBlocks property, which permits the code generator in the
Roslyn compiler to use pointers for JS interop:

XML

<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

2 Warning

The JS interop API requires enabling AllowUnsafeBlocks. Be careful when


implementing your own unsafe code in .NET apps, which can introduce
security and stability risks. For more information, see Unsafe code, pointer
types, and function pointers.

Specify WasmMainJSPath to point to a file on disk. This file is published with the app,
but use of the file isn't required if you're integrating .NET into an existing JS app.

In the following example, the JS file on disk is main.js , but any JS filename is
permissable:

XML

<WasmMainJSPath>main.js</WasmMainJSPath>

Example project file ( .csproj ) after configuration:

XML

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<WasmMainJSPath>main.js</WasmMainJSPath>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>

JavaScript interop on WASM


APIs in the following example are imported from dotnet.js . These APIs enable you to
set up named modules that can be imported into your C# code and call into methods
exposed by your .NET code, including Program.Main .

) Important

"Import" and "export" throughout this article are defined from the perspective of
.NET:

An app imports JS methods so that they can be called from .NET.


The app exports .NET methods so that they can be called from JS.
In the following example:

The dotnet.js file is used to create and start the .NET WebAssembly runtime.
dotnet.js is generated as part of the build output of the app and found in the

AppBundle folder:

bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-


wasm/AppBundle

The {BUILD CONFIGURATION} placeholder is the build configuration (for example,


Debug , Release ), and the {TARGET FRAMEWORK} placeholder is the target framework

(for example, net7.0 ).

) Important

To integrate with an existing app, copy the contents of the AppBundle folder
so that it can be served along with the rest of the app. For production
deployments, publish the app with the dotnet publish -c Release command
in a command shell and deploy the AppBundle folder with the app.

dotnet.create() sets up the .NET WebAssembly runtime.

setModuleImports associates a name with a module of JS functions for import into

.NET. The JS module contains a window.location.href function, which returns the


current page address (URL). The name of the module can be any string (it doesn't
need to be a file name), but it must match the name used with the
JSImportAttribute (explained later in this article). The window.location.href

function is imported into C# and called by the C# method GetHRef . The GetHRef
method is shown later in this section.

exports.MyClass.Greeting() calls into .NET ( MyClass.Greeting ) from JS. The

Greeting C# method returns a string that includes the result of calling the
window.location.href function. The Greeting method is shown later in this

section.

dotnet.run() runs Program.Main .

JS module:

JavaScript
import { dotnet } from './dotnet.js'

const is_browser = typeof window != "undefined";


if (!is_browser) throw new Error(`Expected to be running in a browser`);

const { setModuleImports, getAssemblyExports, getConfig } =


await dotnet.create();

setModuleImports("main.js", {
window: {
location: {
href: () => globalThis.window.location.href
}
}
});

const config = getConfig();


const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

document.getElementById("out").innerHTML = text;
await dotnet.run();

To import a JS function so it can be called from C#, use the new JSImportAttribute on a
matching method signature. The first parameter to the JSImportAttribute is the name of
the JS function to import and the second parameter is the name of the module.

In the following example, the window.location.href function is called from the main.js
module when GetHRef method is called:

C#

[JSImport("window.location.href", "main.js")]
internal static partial string GetHRef();

In the imported method signature, you can use .NET types for parameters and return
values, which are marshalled automatically by the runtime. Use
JSMarshalAsAttribute<T> to control how the imported method parameters are
marshalled. For example, you might choose to marshal a long as
System.Runtime.InteropServices.JavaScript.JSType.Number or
System.Runtime.InteropServices.JavaScript.JSType.BigInt. You can pass
Action/Func<TResult> callbacks as parameters, which are marshalled as callable JS
functions. You can pass both JS and managed object references, and they are marshaled
as proxy objects, keeping the object alive across the boundary until the proxy is garbage
collected. You can also import and export asynchronous methods with a Task result,
which are marshaled as JS promises . Most of the marshalled types work in both
directions, as parameters and as return values, on both imported and exported
methods.

The following table indicates the supported type mappings.

.NET JavaScript Nullable Task ➔ JSMarshalAs Array


Promise optional of

Boolean Boolean ✅ ✅ ✅

Byte Number ✅ ✅ ✅ ✅

Char String ✅ ✅ ✅

Int16 Number ✅ ✅ ✅

Int32 Number ✅ ✅ ✅ ✅

Int64 Number ✅ ✅

Int64 BigInt ✅ ✅

Single Number ✅ ✅ ✅

Double Number ✅ ✅ ✅ ✅

IntPtr Number ✅ ✅ ✅

DateTime Date ✅ ✅

DateTimeOffset Date ✅ ✅

Exception Error ✅ ✅

JSObject Object ✅ ✅ ✅

String String ✅ ✅ ✅

Object Any ✅ ✅

Span<Byte> MemoryView

Span<Int32> MemoryView

Span<Double> MemoryView

ArraySegment<Byte> MemoryView

ArraySegment<Int32> MemoryView
.NET JavaScript Nullable Task ➔ JSMarshalAs Array
Promise optional of

ArraySegment<Double> MemoryView

Task Promise ✅

Action Function

Action<T1> Function

Action<T1, T2> Function

Action<T1, T2, T3> Function

Func<TResult> Function

Func<T1, TResult> Function

Func<T1, T2, TResult> Function

Func<T1, T2, T3, Function


TResult>

The following conditions apply to type mapping and marshalled values:

The Array of column indicates if the .NET type can be marshalled as a JS Array .
Example: C# int[] ( Int32 ) mapped to JS Array of Number s.
When passing a JS value to C# with a value of the wrong type, the framework
throws an exception in most cases. The framework doesn't perform compile-time
type checking in JS.
JSObject , Exception , Task and ArraySegment create GCHandle and a proxy. You

can trigger disposal in developer code or allow .NET garbage collection (GC) to
dispose of the objects later. These types carry significant performance overhead.
Array : Marshaling an array creates a copy of the array in JS or .NET.
MemoryView

MemoryView is a JS class for the .NET WebAssembly runtime to marshal Span and

ArraySegment .
Unlike marshaling an array, marshaling a Span or ArraySegment doesn't create a
copy of the underlying memory.
MemoryView can only be properly instantiated by the .NET WebAssembly

runtime. Therefore, it isn't possible to import a JS function as a .NET method


that has a parameter of Span or ArraySegment .
MemoryView created for a Span is only valid for the duration of the interop call.

As Span is allocated on the call stack, which doesn't persist after the interop call,
it isn't possible to export a .NET method that returns a Span .
MemoryView created for an ArraySegment survives after the interop call and is
useful for sharing a buffer. Calling dispose() on a MemoryView created for an
ArraySegment disposes the proxy and unpins the underlying .NET array. We
recommend calling dispose() in a try-finally block for MemoryView .

Functions accessible on the global namespace can be imported by using the


globalThis prefix in the function name and by using the [JSImport] attribute without
providing a module name. In the following example, console.log is prefixed with
globalThis . The imported function is called by the C# Log method, which accepts a C#
string message ( message ) and marshalls the C# string to a JS String for console.log :

C#

[JSImport("globalThis.console.log")]
internal static partial void Log([JSMarshalAs<JSType.String>] string
message);

To export a .NET method so it can be called from JS, use the JSExportAttribute.

In the following example, the Greeting method returns a string that includes the result
of calling the GetHRef method. As shown earlier, the GetHref C# method calls into JS for
the window.location.href function from the main.js module. window.location.href
returns the current page address (URL):

C#

[JSExport]
internal static string Greeting()
{
var text = $"Hello, World! Greetings from {GetHRef()}";
Console.WriteLine(text);

return text;
}

Experimental workload and project templates


To demonstrate the JS interop functionality and obtain JS interop project templates,
install the wasm-experimental workload:

.NET CLI
dotnet workload install wasm-experimental

The wasm-experimental workload contains two project templates: wasmbrowser and


wasmconsole . These templates are experimental at this time, which means the developer
workflow for the templates is evolving. However, the .NET and JS APIs used in the
templates are supported in .NET 7 and provide a foundation for using .NET on WASM
from JS.

Browser app
You can create a browser app with the wasmbrowser template, which creates a web app
that demonstrates using .NET and JS together in a browser:

.NET CLI

dotnet new wasmbrowser

Build the app from Visual Studio or by using the .NET CLI:

.NET CLI

dotnet build

The built app is in the bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-


wasm/AppBundle directory. The {BUILD CONFIGURATION} placeholder is the build

configuration (for example, Debug , Release ). The {TARGET FRAMEWORK} placeholder is the
target framework moniker (for example, net7.0 ).

Build and run the app from Visual Studio or by using the .NET CLI:

.NET CLI

dotnet run

Alternatively, start any static file server from the AppBundle directory:

.NET CLI

dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/browser-


wasm/AppBundle
In the preceding example, the {TARGET FRAMEWORK} placeholder is the target framework
moniker (for example, net7.0 ).

Node.js console app


You can create a console app with the wasmconsole template, which creates an app that
runs under WASM as a Node.js or V8 console app:

.NET CLI

dotnet new wasmconsole

Build the app from Visual Studio or by using the .NET CLI:

.NET CLI

dotnet build

The built app is in the bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-


wasm/AppBundle directory. The {BUILD CONFIGURATION} placeholder is the build

configuration (for example, Debug , Release ). The {TARGET FRAMEWORK} placeholder is the
target framework moniker (for example, net7.0 ).

Build and run the app from Visual Studio or by using the .NET CLI:

.NET CLI

dotnet run

Alternatively, start any static file server from the AppBundle directory:

node bin/$(Configuration)/{TARGET FRAMEWORK}/browser-wasm/AppBundle/main.mjs

In the preceding example, the {TARGET FRAMEWORK} placeholder is the target framework
moniker (for example, net7.0 ).

Additional resources
API documentation
[JSImport] attribute
[JSExport] attribute
JavaScript JSImport/JSExport interop with ASP.NET Core Blazor WebAssembly
In the dotnet/runtime GitHub repository:
.NET WebAssembly runtime
dotnet.d.ts file (.NET WebAssembly runtime configuration)
Use .NET from any JavaScript app in .NET 7
Use Grunt in ASP.NET Core
Article • 06/03/2022

Grunt is a JavaScript task runner that automates script minification, TypeScript


compilation, code quality "lint" tools, CSS pre-processors, and just about any repetitive
chore that needs doing to support client development. Grunt is fully supported in Visual
Studio.

This example uses an empty ASP.NET Core project as its starting point, to show how to
automate the client build process from scratch.

The finished example cleans the target deployment directory, combines JavaScript files,
checks code quality, condenses JavaScript file content and deploys to the root of your
web application. We will use the following packages:

grunt: The Grunt task runner package.

grunt-contrib-clean: A plugin that removes files or directories.

grunt-contrib-jshint: A plugin that reviews JavaScript code quality.

grunt-contrib-concat: A plugin that joins files into a single file.

grunt-contrib-uglify: A plugin that minifies JavaScript to reduce size.

grunt-contrib-watch: A plugin that watches file activity.

Preparing the application


To begin, set up a new empty web application and add TypeScript example files.
TypeScript files are automatically compiled into JavaScript using default Visual Studio
settings and will be our raw material to process using Grunt.

1. In Visual Studio, create a new ASP.NET Web Application .

2. In the New ASP.NET Project dialog, select the ASP.NET Core Empty template and
click the OK button.

3. In the Solution Explorer, review the project structure. The \src folder includes
empty wwwroot and Dependencies nodes.
4. Add a new folder named TypeScript to your project directory.

5. Before adding any files, make sure that Visual Studio has the option 'compile on
save' for TypeScript files checked. Navigate to Tools > Options > Text Editor >
Typescript > Project:

6. Right-click the TypeScript directory and select Add > New Item from the context
menu. Select the JavaScript file item and name the file Tastes.ts (note the *.ts
extension). Copy the line of TypeScript code below into the file (when you save, a
new Tastes.js file will appear with the JavaScript source).

TypeScript

enum Tastes { Sweet, Sour, Salty, Bitter }

7. Add a second file to the TypeScript directory and name it Food.ts . Copy the code
below into the file.

TypeScript
class Food {
constructor(name: string, calories: number) {
this._name = name;
this._calories = calories;
}

private _name: string;


get Name() {
return this._name;
}

private _calories: number;


get Calories() {
return this._calories;
}

private _taste: Tastes;


get Taste(): Tastes { return this._taste }
set Taste(value: Tastes) {
this._taste = value;
}
}

Configuring NPM
Next, configure NPM to download grunt and grunt-tasks.

1. In the Solution Explorer, right-click the project and select Add > New Item from
the context menu. Select the NPM configuration file item, leave the default name,
package.json , and click the Add button.

2. In the package.json file, inside the devDependencies object braces, enter "grunt".
Select grunt from the Intellisense list and press the Enter key. Visual Studio will
quote the grunt package name, and add a colon. To the right of the colon, select
the latest stable version of the package from the top of the Intellisense list (press
Ctrl-Space if Intellisense doesn't appear).

7 Note
NPM uses semantic versioning to organize dependencies. Semantic
versioning, also known as SemVer, identifies packages with the numbering
scheme <major>.<minor>.<patch>. Intellisense simplifies semantic
versioning by showing only a few common choices. The top item in the
Intellisense list (0.4.5 in the example above) is considered the latest stable
version of the package. The caret (^) symbol matches the most recent major
version and the tilde (~) matches the most recent minor version. See the NPM
semver version parser reference as a guide to the full expressivity that
SemVer provides.

3. Add more dependencies to load grunt-contrib-* packages for clean, jshint, concat,
uglify, and watch as shown in the example below. The versions don't need to
match the example.

JSON

"devDependencies": {
"grunt": "0.4.5",
"grunt-contrib-clean": "0.6.0",
"grunt-contrib-jshint": "0.11.0",
"grunt-contrib-concat": "0.5.1",
"grunt-contrib-uglify": "0.8.0",
"grunt-contrib-watch": "0.6.1"
}

4. Save the package.json file.

The packages for each devDependencies item will download, along with any files that
each package requires. You can find the package files in the node_modules directory by
enabling the Show All Files button in Solution Explorer.

7 Note

If you need to, you can manually restore dependencies in Solution Explorer by
right-clicking on Dependencies\NPM and selecting the Restore Packages menu
option.
Configuring Grunt
Grunt is configured using a manifest named Gruntfile.js that defines, loads and
registers tasks that can be run manually or configured to run automatically based on
events in Visual Studio.

1. Right-click the project and select Add > New Item. Select the JavaScript File item
template, change the name to Gruntfile.js , and click the Add button.

2. Add the following code to Gruntfile.js . The initConfig function sets options for
each package, and the remainder of the module loads and register tasks.

JavaScript

module.exports = function (grunt) {


grunt.initConfig({
});
};

3. Inside the initConfig function, add options for the clean task as shown in the
example Gruntfile.js below. The clean task accepts an array of directory strings.
This task removes files from wwwroot/lib and removes the entire /temp directory.

JavaScript

module.exports = function (grunt) {


grunt.initConfig({
clean: ["wwwroot/lib/*", "temp/"],
});
};

4. Below the initConfig function, add a call to grunt.loadNpmTasks . This will make
the task runnable from Visual Studio.
JavaScript

grunt.loadNpmTasks("grunt-contrib-clean");

5. Save Gruntfile.js . The file should look something like the screenshot below.

6. Right-click Gruntfile.js and select Task Runner Explorer from the context menu.
The Task Runner Explorer window will open.

7. Verify that clean shows under Tasks in the Task Runner Explorer.

8. Right-click the clean task and select Run from the context menu. A command
window displays progress of the task.
7 Note

There are no files or directories to clean yet. If you like, you can manually
create them in the Solution Explorer and then run the clean task as a test.

9. In the initConfig function, add an entry for concat using the code below.

The src property array lists files to combine, in the order that they should be
combined. The dest property assigns the path to the combined file that's
produced.

JavaScript

concat: {
all: {
src: ['TypeScript/Tastes.js', 'TypeScript/Food.js'],
dest: 'temp/combined.js'
}
},

7 Note

The all property in the code above is the name of a target. Targets are used
in some Grunt tasks to allow multiple build environments. You can view the
built-in targets using IntelliSense or assign your own.

10. Add the jshint task using the code below.

The jshint code-quality utility is run against every JavaScript file found in the temp
directory.

JavaScript
jshint: {
files: ['temp/*.js'],
options: {
'-W069': false,
}
},

7 Note

The option "-W069" is an error produced by jshint when JavaScript uses


bracket syntax to assign a property instead of dot notation, i.e.
Tastes["Sweet"] instead of Tastes.Sweet . The option turns off the warning to
allow the rest of the process to continue.

11. Add the uglify task using the code below.

The task minifies the combined.js file found in the temp directory and creates the
result file in wwwroot/lib following the standard naming convention <file
name>.min.js.

JavaScript

uglify: {
all: {
src: ['temp/combined.js'],
dest: 'wwwroot/lib/combined.min.js'
}
},

12. Under the call to grunt.loadNpmTasks that loads grunt-contrib-clean , include the
same call for jshint, concat, and uglify using the code below.

JavaScript

grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');

13. Save Gruntfile.js . The file should look something like the example below.
14. Notice that the Task Runner Explorer Tasks list includes clean , concat , jshint and
uglify tasks. Run each task in order and observe the results in Solution Explorer.

Each task should run without errors.

The concat task creates a new combined.js file and places it into the temp
directory. The jshint task simply runs and doesn't produce output. The uglify
task creates a new combined.min.js file and places it into wwwroot/lib. On
completion, the solution should look something like the screenshot below:
7 Note

For more information on the options for each package, visit


https://www.npmjs.com/ and lookup the package name in the search box
on the main page. For example, you can look up the grunt-contrib-clean
package to get a documentation link that explains all of its parameters.

All together now


Use the Grunt registerTask() method to run a series of tasks in a particular sequence.
For example, to run the example steps above in the order clean -> concat -> jshint ->
uglify, add the code below to the module. The code should be added to the same level
as the loadNpmTasks() calls, outside initConfig.

JavaScript

grunt.registerTask("all", ['clean', 'concat', 'jshint', 'uglify']);

The new task shows up in Task Runner Explorer under Alias Tasks. You can right-click and
run it just as you would other tasks. The all task will run clean , concat , jshint and
uglify , in order.
Watching for changes
A watch task keeps an eye on files and directories. The watch triggers tasks
automatically if it detects changes. Add the code below to initConfig to watch for
changes to *.js files in the TypeScript directory. If a JavaScript file is changed, watch will
run the all task.

JavaScript

watch: {
files: ["TypeScript/*.js"],
tasks: ["all"]
}

Add a call to loadNpmTasks() to show the watch task in Task Runner Explorer.

JavaScript

grunt.loadNpmTasks('grunt-contrib-watch');

Right-click the watch task in Task Runner Explorer and select Run from the context
menu. The command window that shows the watch task running will display a
"Waiting…" message. Open one of the TypeScript files, add a space, and then save the
file. This will trigger the watch task and trigger the other tasks to run in order. The
screenshot below shows a sample run.
Binding to Visual Studio events
Unless you want to manually start your tasks every time you work in Visual Studio, bind
tasks to Before Build, After Build, Clean, and Project Open events.

Bind watch so that it runs every time Visual Studio opens. In Task Runner Explorer, right-
click the watch task and select Bindings > Project Open from the context menu.

Unload and reload the project. When the project loads again, the watch task starts
running automatically.

Summary
Grunt is a powerful task runner that can be used to automate most client-build tasks.
Grunt leverages NPM to deliver its packages, and features tooling integration with
Visual Studio. Visual Studio's Task Runner Explorer detects changes to configuration files
and provides a convenient interface to run tasks, view running tasks, and bind tasks to
Visual Studio events.
Bundle and minify static assets in
ASP.NET Core
Article • 11/17/2022

By Scott Addie and David Pine

This article explains the benefits of applying bundling and minification, including how
these features can be used with ASP.NET Core web apps.

What is bundling and minification


Bundling and minification are two distinct performance optimizations you can apply in a
web app. Used together, bundling and minification improve performance by reducing
the number of server requests and reducing the size of the requested static assets.

Bundling and minification primarily improve the first page request load time. Once a
web page has been requested, the browser caches the static assets (JavaScript, CSS, and
images). So, bundling and minification don't improve performance when requesting the
same page, or pages, on the same site requesting the same assets. If the expires header
isn't set correctly on the assets and if bundling and minification isn't used, the browser's
freshness heuristics mark the assets stale after a few days. Additionally, the browser
requires a validation request for each asset. In this case, bundling and minification
provide a performance improvement even after the first page request.

Bundling
Bundling combines multiple files into a single file. Bundling reduces the number of
server requests that are necessary to render a web asset, such as a web page. You can
create any number of individual bundles specifically for CSS, JavaScript, etc. Fewer files
mean fewer HTTP requests from the browser to the server or from the service providing
your application. This results in improved first page load performance.

Minification
Minification removes unnecessary characters from code without altering functionality.
The result is a significant size reduction in requested assets (such as CSS, images, and
JavaScript files). Common side effects of minification include shortening variable names
to one character and removing comments and unnecessary whitespace.
Consider the following JavaScript function:

JavaScript

AddAltToImg = function (imageTagAndImageID, imageContext) {


///<signature>
///<summary> Adds an alt tab to the image
// </summary>
//<param name="imgElement" type="String">The image selector.</param>
//<param name="ContextForImage" type="String">The image context.</param>
///</signature>
var imageElement = $(imageTagAndImageID, imageContext);
imageElement.attr('alt', imageElement.attr('id').replace(/ID/, ''));
}

Minification reduces the function to the following:

JavaScript

AddAltToImg=function(t,a){var
r=$(t,a);r.attr("alt",r.attr("id").replace(/ID/,""))};

In addition to removing the comments and unnecessary whitespace, the following


parameter and variable names were renamed as follows:

Original Renamed

imageTagAndImageID t

imageContext a

imageElement r

Impact of bundling and minification


The following table outlines differences between individually loading assets and using
bundling and minification for a typical web app.

Action Without B/M With B/M Reduction

File Requests 18 7 61%

Bytes Transferred (KB) 265 156 41%

Load Time (ms) 2360 885 63%


The load time improved, but this example ran locally. Greater performance gains are
realized when using bundling and minification with assets transferred over a network.

The test app used to generate the figures in the preceding table demonstrates typical
improvements that might not apply to a given app. We recommend testing an app to
determine if bundling and minification yields an improved load time.

Choose a bundling and minification strategy


ASP.NET Core is compatible with WebOptimizer, an open-source bundling and
minification solution. For set up instructions and sample projects, see WebOptimizer .
ASP.NET Core doesn't provide a native bundling and minification solution.

Third-party tools, such as Gulp and Webpack , provide workflow automation for
bundling and minification, as well as linting and image optimization. By using bundling
and minification, the minified files are created prior to the app's deployment. Bundling
and minifying before deployment provides the advantage of reduced server load.
However, it's important to recognize that bundling and minification increases build
complexity and only works with static files.

Environment-based bundling and minification


As a best practice, the bundled and minified files of your app should be used in a
production environment. During development, the original files make for easier
debugging of the app.

Specify which files to include in your pages by using the Environment Tag Helper in your
views. The Environment Tag Helper only renders its contents when running in specific
environments.

The following environment tag renders the unprocessed CSS files when running in the
Development environment:

CSHTML

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>

The following environment tag renders the bundled and minified CSS files when running
in an environment other than Development . For example, running in Production or
Staging triggers the rendering of these stylesheets:

CSHTML

<environment exclude="Development">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-
property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-
version="true" />
</environment>

Additional resources
Use multiple environments
Tag Helpers
Browser Link in ASP.NET Core
Article • 07/12/2022

By Nicolò Carandini and Tom Dykstra

Browser Link is a Visual Studio feature. It creates a communication channel between the
development environment and one or more web browsers. Use Browser Link to:

Refresh your web app in several browsers at once.


Test across multiple browsers with specific settings such as screen sizes.
Select UI elements in browsers in real-time, see what markup and source it's
correlated to in Visual Studio.
Conduct real-time browser test automation. Browser Link is also extensible.

Browser Link setup


Add the Microsoft.VisualStudio.Web.BrowserLink package to your project. For
ASP.NET Core Razor Pages or MVC projects, also enable runtime compilation of Razor
( .cshtml ) files as described in Razor file compilation in ASP.NET Core. Razor syntax
changes are applied only when runtime compilation has been enabled.

Configuration
Call UseBrowserLink in the Startup.Configure method:

C#

app.UseBrowserLink();

The UseBrowserLink call is typically placed inside an if block that only enables Browser
Link in the Development environment. For example:

C#

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}

For more information, see Use multiple environments in ASP.NET Core.


How to use Browser Link
When you have an ASP.NET Core project open, Visual Studio shows the Browser Link
toolbar control next to the Debug Target toolbar control:

From the Browser Link toolbar control, you can:

Refresh the web app in several browsers at once.


Open the Browser Link Dashboard.
Enable or disable Browser Link. Note: Browser Link is disabled by default in Visual
Studio.
Enable or disable CSS Auto-Sync.

Refresh the web app in several browsers at


once
To choose a single web browser to launch when starting the project, use the drop-down
menu in the Debug Target toolbar control:

To open multiple browsers at once, choose Browse with... from the same drop-down.
Hold down the Ctrl key to select the browsers you want, and then click Browse:
The following screenshot shows Visual Studio with the Index view open and two open
browsers:

Hover over the Browser Link toolbar control to see the browsers that are connected to
the project:
Change the Index view, and all connected browsers are updated when you click the
Browser Link refresh button:

Browser Link also works with browsers that you launch from outside Visual Studio and
navigate to the app URL.

The Browser Link Dashboard


Open the Browser Link Dashboard window from the Browser Link drop down menu to
manage the connection with open browsers:
If no browser is connected, you can start a non-debugging session by selecting the View
in Browser link:

Otherwise, the connected browsers are shown with the path to the page that each
browser is showing:
You can also click on an individual browser name to refresh only that browser.

Enable or disable Browser Link


When you re-enable Browser Link after disabling it, you must refresh the browsers to
reconnect them.

Enable or disable CSS Auto-Sync


When CSS Auto-Sync is enabled, connected browsers are automatically refreshed when
you make any change to CSS files.

How it works
Browser Link uses SignalR to create a communication channel between Visual Studio
and the browser. When Browser Link is enabled, Visual Studio acts as a SignalR server
that multiple clients (browsers) can connect to. Browser Link also registers a middleware
component in the ASP.NET Core request pipeline. This component injects special
<script> references into every page request from the server. You can see the script

references by selecting View source in the browser and scrolling to the end of the
<body> tag content:

HTML

<!-- Visual Studio Browser Link -->


<script type="application/json" id="__browserLink_initializationData">
{"requestId":"a717d5a07c1741949a7cefd6fa2bad08","requestMappingFromServer":f
alse}
</script>
<script type="text/javascript"
src="http://localhost:54139/b6e36e429d034f578ebccd6a79bf19bf/browserLink"
async="async"></script>
<!-- End Browser Link -->
</body>

Your source files aren't modified. The middleware component injects the script
references dynamically.

Because the browser-side code is all JavaScript, it works on all browsers that SignalR
supports without requiring a browser plug-in.
Session and state management in
ASP.NET Core
Article • 02/14/2023

By Rick Anderson , Kirk Larkin , and Diana LaRose

HTTP is a stateless protocol. By default, HTTP requests are independent messages that
don't retain user values. This article describes several approaches to preserve user data
between requests.

State management
State can be stored using several approaches. Each approach is described later in this
article.

Storage approach Storage mechanism

Cookies HTTP cookies. May include data stored using server-side app code.

Session state HTTP cookies and server-side app code

TempData HTTP cookies or session state

Query strings HTTP query strings

Hidden fields HTTP form fields

HttpContext.Items Server-side app code

Cache Server-side app code

SignalR/Blazor Server and HTTP context-based


state management
SignalR apps shouldn't use session state and other state management approaches that
rely upon a stable HTTP context to store information. SignalR apps can store per-
connection state in Context.Items in the hub. For more information and alternative state
management approaches for Blazor Server apps, see ASP.NET Core Blazor state
management.

Cookies
Cookies store data across requests. Because cookies are sent with every request, their
size should be kept to a minimum. Ideally, only an identifier should be stored in a cookie
with the data stored by the app. Most browsers restrict cookie size to 4096 bytes. Only a
limited number of cookies are available for each domain.

Because cookies are subject to tampering, they must be validated by the app. Cookies
can be deleted by users and expire on clients. However, cookies are generally the most
durable form of data persistence on the client.

Cookies are often used for personalization, where content is customized for a known
user. The user is only identified and not authenticated in most cases. The cookie can
store the user's name, account name, or unique user ID such as a GUID. The cookie can
be used to access the user's personalized settings, such as their preferred website
background color.

See the European Union General Data Protection Regulations (GDPR) when issuing
cookies and dealing with privacy concerns. For more information, see General Data
Protection Regulation (GDPR) support in ASP.NET Core.

Session state
Session state is an ASP.NET Core scenario for storage of user data while the user
browses a web app. Session state uses a store maintained by the app to persist data
across requests from a client. The session data is backed by a cache and considered
ephemeral data. The site should continue to function without the session data. Critical
application data should be stored in the user database and cached in session only as a
performance optimization.

Session isn't supported in SignalR apps because a SignalR Hub may execute
independent of an HTTP context. For example, this can occur when a long polling
request is held open by a hub beyond the lifetime of the request's HTTP context.

ASP.NET Core maintains session state by providing a cookie to the client that contains a
session ID. The cookie session ID:

Is sent to the app with each request.


Is used by the app to fetch the session data.

Session state exhibits the following behaviors:

The session cookie is specific to the browser. Sessions aren't shared across
browsers.
Session cookies are deleted when the browser session ends.
If a cookie is received for an expired session, a new session is created that uses the
same session cookie.
Empty sessions aren't retained. The session must have at least one value set to
persist the session across requests. When a session isn't retained, a new session ID
is generated for each new request.
The app retains a session for a limited time after the last request. The app either
sets the session timeout or uses the default value of 20 minutes. Session state is
ideal for storing user data:
That's specific to a particular session.
Where the data doesn't require permanent storage across sessions.
Session data is deleted either when the ISession.Clear implementation is called or
when the session expires.
There's no default mechanism to inform app code that a client browser has been
closed or when the session cookie is deleted or expired on the client.
Session state cookies aren't marked essential by default. Session state isn't
functional unless tracking is permitted by the site visitor. For more information, see
General Data Protection Regulation (GDPR) support in ASP.NET Core.
Note: There is no replacement for the cookieless session feature from the ASP.NET
Framework because it's considered insecure and can lead to session fixation
attacks.

2 Warning

Don't store sensitive data in session state. The user might not close the browser
and clear the session cookie. Some browsers maintain valid session cookies across
browser windows. A session might not be restricted to a single user. The next user
might continue to browse the app with the same session cookie.

The in-memory cache provider stores session data in the memory of the server where
the app resides. In a server farm scenario:

Use sticky sessions to tie each session to a specific app instance on an individual
server. Azure App Service uses Application Request Routing (ARR) to enforce
sticky sessions by default. However, sticky sessions can affect scalability and
complicate web app updates. A better approach is to use a Redis or SQL Server
distributed cache, which doesn't require sticky sessions. For more information, see
Distributed caching in ASP.NET Core.
The session cookie is encrypted via IDataProtector. Data Protection must be
properly configured to read session cookies on each machine. For more
information, see ASP.NET Core Data Protection Overview and Key storage
providers.
Configure session state
The Microsoft.AspNetCore.Session package:

Is included implicitly by the framework.


Provides middleware for managing session state.

To enable the session middleware, Program.cs must contain:

Any of the IDistributedCache memory caches. The IDistributedCache


implementation is used as a backing store for session. For more information, see
Distributed caching in ASP.NET Core.
A call to AddSession
A call to UseSession

The following code shows how to set up the in-memory session provider with a default
in-memory implementation of IDistributedCache :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();
app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

The preceding code sets a short timeout to simplify testing.

The order of middleware is important. Call UseSession after UseRouting and before
MapRazorPages and MapDefaultControllerRoute . See Middleware Ordering.

HttpContext.Session is available after session state is configured.

HttpContext.Session can't be accessed before UseSession has been called.

A new session with a new session cookie can't be created after the app has begun
writing to the response stream. The exception is recorded in the web server log and not
displayed in the browser.

Load session state asynchronously


The default session provider in ASP.NET Core loads session records from the underlying
IDistributedCache backing store asynchronously only if the ISession.LoadAsync method
is explicitly called before the TryGetValue, Set, or Remove methods. If LoadAsync isn't
called first, the underlying session record is loaded synchronously, which can incur a
performance penalty at scale.

To have apps enforce this pattern, wrap the DistributedSessionStore and


DistributedSession implementations with versions that throw an exception if the
LoadAsync method isn't called before TryGetValue , Set , or Remove . Register the
wrapped versions in the services container.

Session options
To override session defaults, use SessionOptions.

Option Description

Cookie Determines the settings used to create the cookie. Name defaults to
SessionDefaults.CookieName ( .AspNetCore.Session ). Path defaults to
SessionDefaults.CookiePath ( / ). SameSite defaults to SameSiteMode.Lax ( 1 ).
HttpOnly defaults to true . IsEssential defaults to false .
Option Description

IdleTimeout The IdleTimeout indicates how long the session can be idle before its contents are
abandoned. Each session access resets the timeout. This setting only applies to the
content of the session, not the cookie. The default is 20 minutes.

IOTimeout The maximum amount of time allowed to load a session from the store or to
commit it back to the store. This setting may only apply to asynchronous
operations. This timeout can be disabled using InfiniteTimeSpan. The default is 1
minute.

Session uses a cookie to track and identify requests from a single browser. By default,
this cookie is named .AspNetCore.Session , and it uses a path of / . Because the cookie
default doesn't specify a domain, it isn't made available to the client-side script on the
page (because HttpOnly defaults to true ).

To override cookie session defaults, use SessionOptions:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
options.Cookie.Name = ".AdventureWorks.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.IsEssential = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();
app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

The app uses the IdleTimeout property to determine how long a session can be idle
before its contents in the server's cache are abandoned. This property is independent of
the cookie expiration. Each request that passes through the Session Middleware resets
the timeout.

Session state is non-locking. If two requests simultaneously attempt to modify the


contents of a session, the last request overrides the first. Session is implemented as a
coherent session, which means that all the contents are stored together. When two
requests seek to modify different session values, the last request may override session
changes made by the first.

Set and get Session values


Session state is accessed from a Razor Pages PageModel class or MVC Controller class
with HttpContext.Session. This property is an ISession implementation.

The ISession implementation provides several extension methods to set and retrieve
integer and string values. The extension methods are in the Microsoft.AspNetCore.Http
namespace.

ISession extension methods:

Get(ISession, String)
GetInt32(ISession, String)
GetString(ISession, String)
SetInt32(ISession, String, Int32)
SetString(ISession, String, String)

The following example retrieves the session value for the IndexModel.SessionKeyName
key ( _Name in the sample app) in a Razor Pages page:

C#

@page
@using Microsoft.AspNetCore.Http
@model IndexModel

...

Name: @HttpContext.Session.GetString(IndexModel.SessionKeyName)
The following example shows how to set and get an integer and a string:

C#

public class IndexModel : PageModel


{
public const string SessionKeyName = "_Name";
public const string SessionKeyAge = "_Age";

private readonly ILogger<IndexModel> _logger;

public IndexModel(ILogger<IndexModel> logger)


{
_logger = logger;
}

public void OnGet()


{
if
(string.IsNullOrEmpty(HttpContext.Session.GetString(SessionKeyName)))
{
HttpContext.Session.SetString(SessionKeyName, "The Doctor");
HttpContext.Session.SetInt32(SessionKeyAge, 73);
}
var name = HttpContext.Session.GetString(SessionKeyName);
var age = HttpContext.Session.GetInt32(SessionKeyAge).ToString();

_logger.LogInformation("Session Name: {Name}", name);


_logger.LogInformation("Session Age: {Age}", age);
}
}

The following markup displays the session values on a Razor Page:

CSHTML

@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<div class="text-center">
<p><b>Name:</b> @HttpContext.Session.GetString("_Name");<b>Age:

</b> @HttpContext.Session.GetInt32("_Age").ToString()</p>
</div>
All session data must be serialized to enable a distributed cache scenario, even when
using the in-memory cache. String and integer serializers are provided by the extension
methods of ISession. Complex types must be serialized by the user using another
mechanism, such as JSON.

Use the following sample code to serialize objects:

C#

public static class SessionExtensions


{
public static void Set<T>(this ISession session, string key, T value)
{
session.SetString(key, JsonSerializer.Serialize(value));
}

public static T? Get<T>(this ISession session, string key)


{
var value = session.GetString(key);
return value == null ? default : JsonSerializer.Deserialize<T>
(value);
}
}

The following example shows how to set and get a serializable object with the
SessionExtensions class:

C#

using Microsoft.AspNetCore.Mvc.RazorPages;
using Web.Extensions; // SessionExtensions

namespace SessionSample.Pages
{
public class Index6Model : PageModel
{
const string SessionKeyTime = "_Time";
public string? SessionInfo_SessionTime { get; private set; }
private readonly ILogger<Index6Model> _logger;

public Index6Model(ILogger<Index6Model> logger)


{
_logger = logger;
}

public void OnGet()


{
var currentTime = DateTime.Now;

// Requires SessionExtensions from sample.


if (HttpContext.Session.Get<DateTime>(SessionKeyTime) ==
default)
{
HttpContext.Session.Set<DateTime>(SessionKeyTime,
currentTime);
}
_logger.LogInformation("Current Time: {Time}", currentTime);
_logger.LogInformation("Session Time: {Time}",
HttpContext.Session.Get<DateTime>
(SessionKeyTime));

}
}
}

2 Warning

Storing a live object in the session should be used with caution, as there are many
problems that can occur with serialized objects. For more information, see Sessions
should be allowed to store objects (dotnet/aspnetcore #18159) .

TempData
ASP.NET Core exposes the Razor Pages TempData or Controller TempData. This property
stores data until it's read in another request. The Keep(String) and Peek(string) methods
can be used to examine the data without deletion at the end of the request. Keep marks
all items in the dictionary for retention. TempData is:

Useful for redirection when data is required for more than a single request.
Implemented by TempData providers using either cookies or session state.

TempData samples
Consider the following page that creates a customer:

C#

public class CreateModel : PageModel


{
private readonly RazorPagesContactsContext _context;

public CreateModel(RazorPagesContactsContext context)


{
_context = context;
}
public IActionResult OnGet()
{
return Page();
}

[TempData]
public string Message { get; set; }

[BindProperty]
public Customer Customer { get; set; }

public async Task<IActionResult> OnPostAsync()


{
if (!ModelState.IsValid)
{
return Page();
}

_context.Customer.Add(Customer);
await _context.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";

return RedirectToPage("./IndexPeek");
}
}

The following page displays TempData["Message"] :

CSHTML

@page
@model IndexModel

<h1>Peek Contacts</h1>

@{
if (TempData.Peek("Message") != null)
{
<h3>Message: @TempData.Peek("Message")</h3>
}
}

@*Content removed for brevity.*@

In the preceding markup, at the end of the request, TempData["Message"] is not deleted
because Peek is used. Refreshing the page displays the contents of
TempData["Message"] .
The following markup is similar to the preceding code, but uses Keep to preserve the
data at the end of the request:

CSHTML

@page
@model IndexModel

<h1>Contacts Keep</h1>

@{
if (TempData["Message"] != null)
{
<h3>Message: @TempData["Message"]</h3>
}
TempData.Keep("Message");
}

@*Content removed for brevity.*@

Navigating between the IndexPeek and IndexKeep pages won't delete


TempData["Message"] .

The following code displays TempData["Message"] , but at the end of the request,
TempData["Message"] is deleted:

CSHTML

@page
@model IndexModel

<h1>Index no Keep or Peek</h1>

@{
if (TempData["Message"] != null)
{
<h3>Message: @TempData["Message"]</h3>
}
}

@*Content removed for brevity.*@

TempData providers
The cookie-based TempData provider is used by default to store TempData in cookies.
The cookie data is encrypted using IDataProtector, encoded with Base64UrlTextEncoder,
then chunked. The maximum cookie size is less than 4096 bytes due to encryption
and chunking. The cookie data isn't compressed because compressing encrypted data
can lead to security problems such as the CRIME and BREACH attacks. For more
information on the cookie-based TempData provider, see CookieTempDataProvider.

Choose a TempData provider


Choosing a TempData provider involves several considerations, such as:

Does the app already use session state? If so, using the session state TempData
provider has no additional cost to the app beyond the size of the data.
Does the app use TempData only sparingly for relatively small amounts of data, up
to 500 bytes? If so, the cookie TempData provider adds a small cost to each
request that carries TempData. If not, the session state TempData provider can be
beneficial to avoid round-tripping a large amount of data in each request until the
TempData is consumed.
Does the app run in a server farm on multiple servers? If so, there's no additional
configuration required to use the cookie TempData provider outside of Data
Protection. For more information, see ASP.NET Core Data Protection Overview and
Key storage providers.

Most web clients such as web browsers enforce limits on the maximum size of each
cookie and the total number of cookies. When using the cookie TempData provider,
verify the app won't exceed these limits . Consider the total size of the data. Account
for increases in cookie size due to encryption and chunking.

Configure the TempData provider


The cookie-based TempData provider is enabled by default.

To enable the session-based TempData provider, use the


AddSessionStateTempDataProvider extension method. Only one call to
AddSessionStateTempDataProvider is required:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages()
.AddSessionStateTempDataProvider();
builder.Services.AddControllersWithViews()
.AddSessionStateTempDataProvider();
builder.Services.AddSession();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();

Query strings
A limited amount of data can be passed from one request to another by adding it to the
new request's query string. This is useful for capturing state in a persistent manner that
allows links with embedded state to be shared through email or social networks.
Because URL query strings are public, never use query strings for sensitive data.

In addition to unintended sharing, including data in query strings can expose the app to
Cross-Site Request Forgery (CSRF) attacks. Any preserved session state must protect
against CSRF attacks. For more information, see Prevent Cross-Site Request Forgery
(XSRF/CSRF) attacks in ASP.NET Core.

Hidden fields
Data can be saved in hidden form fields and posted back on the next request. This is
common in multi-page forms. Because the client can potentially tamper with the data,
the app must always revalidate the data stored in hidden fields.

HttpContext.Items
The HttpContext.Items collection is used to store data while processing a single request.
The collection's contents are discarded after a request is processed. The Items
collection is often used to allow components or middleware to communicate when they
operate at different points in time during a request and have no direct way to pass
parameters.

In the following example, middleware adds isVerified to the Items collection:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

ILogger logger = app.Logger;

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


{
// context.Items["isVerified"] is null
logger.LogInformation($"Before setting: Verified:
{context.Items["isVerified"]}");
context.Items["isVerified"] = true;
await next.Invoke();
});

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


{
// context.Items["isVerified"] is true
logger.LogInformation($"Next: Verified: {context.Items["isVerified"]}");
await next.Invoke();
});

app.MapGet("/", async context =>


{
await context.Response.WriteAsync($"Verified:
{context.Items["isVerified"]}");
});

app.Run();

For middleware that's only used in a single app, it's unlikely that using a fixed string
key would cause a key collision. However, to avoid the possibility of a key collision
altogether, an object can be used as an item key. This approach is particularly useful for
middleware that's shared between apps and also has the advantage of eliminating the
use of key strings in the code. The following example shows how to use an object key
defined in a middleware class:

C#
public class HttpContextItemsMiddleware
{
private readonly RequestDelegate _next;
public static readonly object HttpContextItemsMiddlewareKey = new();

public HttpContextItemsMiddleware(RequestDelegate next)


{
_next = next;
}

public async Task Invoke(HttpContext httpContext)


{
httpContext.Items[HttpContextItemsMiddlewareKey] = "K-9";

await _next(httpContext);
}
}

public static class HttpContextItemsMiddlewareExtensions


{
public static IApplicationBuilder
UseHttpContextItemsMiddleware(this IApplicationBuilder app)
{
return app.UseMiddleware<HttpContextItemsMiddleware>();
}
}

Other code can access the value stored in HttpContext.Items using the key exposed by
the middleware class:

C#

public class Index2Model : PageModel


{
private readonly ILogger<Index2Model> _logger;

public Index2Model(ILogger<Index2Model> logger)


{
_logger = logger;
}

public void OnGet()


{
HttpContext.Items

.TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey,
out var middlewareSetValue);

_logger.LogInformation("Middleware value {MV}",


middlewareSetValue?.ToString() ?? "Middleware value not set!");
}
}
Cache
Caching is an efficient way to store and retrieve data. The app can control the lifetime of
cached items. For more information, see Response caching in ASP.NET Core.

Cached data isn't associated with a specific request, user, or session. Do not cache user-
specific data that may be retrieved by other user requests.

To cache application wide data, see Cache in-memory in ASP.NET Core.

Checking session state


ISession.IsAvailable is intended to check for transient failures. Calling IsAvailable
before the session middleware runs throws an InvalidOperationException .

Libraries that need to test session availability can use


HttpContext.Features.Get<ISessionFeature>()?.Session != null .

Common errors
"Unable to resolve service for type
'Microsoft.Extensions.Caching.Distributed.IDistributedCache' while attempting to
activate 'Microsoft.AspNetCore.Session.DistributedSessionStore'."

This is typically caused by failing to configure at least one IDistributedCache


implementation. For more information, see Distributed caching in ASP.NET Core
and Cache in-memory in ASP.NET Core.

If the session middleware fails to persist a session:

The middleware logs the exception and the request continues normally.
This leads to unpredictable behavior.

The session middleware can fail to persist a session if the backing store isn't available.
For example, a user stores a shopping cart in session. The user adds an item to the cart
but the commit fails. The app doesn't know about the failure so it reports to the user
that the item was added to their cart, which isn't true.

The recommended approach to check for errors is to call await


feature.Session.CommitAsync when the app is done writing to the session. CommitAsync
throws an exception if the backing store is unavailable. If CommitAsync fails, the app can
process the exception. LoadAsync throws under the same conditions when the data
store is unavailable.

Additional resources
View or download sample code (how to download)

Host ASP.NET Core in a web farm


Layout in ASP.NET Core
Article • 06/03/2022

By Steve Smith and Dave Brock

Pages and views frequently share visual and programmatic elements. This article
demonstrates how to:

Use common layouts.


Share directives.
Run common code before rendering pages or views.

This document discusses layouts for the two different approaches to ASP.NET Core
MVC: Razor Pages and controllers with views. For this topic, the differences are minimal:

Razor Pages are in the Pages folder.


Controllers with views uses a Views folder for views.

What is a Layout
Most web apps have a common layout that provides the user with a consistent
experience as they navigate from page to page. The layout typically includes common
user interface elements such as the app header, navigation or menu elements, and
footer.
Common HTML structures such as scripts and stylesheets are also frequently used by
many pages within an app. All of these shared elements may be defined in a layout file,
which can then be referenced by any view used within the app. Layouts reduce duplicate
code in views.

By convention, the default layout for an ASP.NET Core app is named _Layout.cshtml .
The layout files for new ASP.NET Core projects created with the templates are:

Razor Pages: Pages/Shared/_Layout.cshtml

Controller with views: Views/Shared/_Layout.cshtml

The layout defines a top level template for views in the app. Apps don't require a layout.
Apps can define more than one layout, with different views specifying different layouts.

The following code shows the layout file for a template created project with a controller
and views:

CSHTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css"
/>
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-
property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-
version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-
toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-
brand">WebApplication1</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>

<partial name="_CookieConsentPartial" />

<div class="container body-content">


@RenderBody()
<hr />
<footer>
<p>&copy; 2018 - WebApplication1</p>
</footer>
</div>

<environment include="Development">
<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>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-
3.3.1.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-
tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
</script>
<script
src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn &&
window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-
Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)


</body>
</html>

Specifying a Layout
Razor views have a Layout property. Individual views specify a layout by setting this
property:

CSHTML

@{
Layout = "_Layout";
}

The layout specified can use a full path (for example, /Pages/Shared/_Layout.cshtml or
/Views/Shared/_Layout.cshtml ) or a partial name (example: _Layout ). When a partial
name is provided, the Razor view engine searches for the layout file using its standard
discovery process. The folder where the handler method (or controller) exists is searched
first, followed by the Shared folder. This discovery process is identical to the process
used to discover partial views.

By default, every layout must call RenderBody . Wherever the call to RenderBody is placed,
the contents of the view will be rendered.
Sections
A layout can optionally reference one or more sections, by calling RenderSection .
Sections provide a way to organize where certain page elements should be placed. Each
call to RenderSection can specify whether that section is required or optional:

HTML

<script type="text/javascript" src="~/scripts/global.js"></script>

@RenderSection("Scripts", required: false)

If a required section isn't found, an exception is thrown. Individual views specify the
content to be rendered within a section using the @section Razor syntax. If a page or
view defines a section, it must be rendered (or an error will occur).

An example @section definition in Razor Pages view:

HTML

@section Scripts {
<script type="text/javascript" src="~/scripts/main.js"></script>
}

In the preceding code, scripts/main.js is added to the scripts section on a page or


view. Other pages or views in the same app might not require this script and wouldn't
define a scripts section.

The following markup uses the Partial Tag Helper to render


_ValidationScriptsPartial.cshtml :

HTML

@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

The preceding markup was generated by scaffolding Identity.

Sections defined in a page or view are available only in its immediate layout page. They
cannot be referenced from partials, view components, or other parts of the view system.

Ignoring sections
By default, the body and all sections in a content page must all be rendered by the
layout page. The Razor view engine enforces this by tracking whether the body and each
section have been rendered.

To instruct the view engine to ignore the body or sections, call the IgnoreBody and
IgnoreSection methods.

The body and every section in a Razor page must be either rendered or ignored.

Importing Shared Directives


Views and pages can use Razor directives to import namespaces and use dependency
injection. Directives shared by many views may be specified in a common
_ViewImports.cshtml file. The _ViewImports file supports the following directives:

@addTagHelper
@removeTagHelper

@tagHelperPrefix

@using
@model

@inherits
@inject

@namespace

The file doesn't support other Razor features, such as functions and section definitions.

A sample _ViewImports.cshtml file:

CSHTML

@using WebApplication1
@using WebApplication1.Models
@using WebApplication1.Models.AccountViewModels
@using WebApplication1.Models.ManageViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The _ViewImports.cshtml file for an ASP.NET Core MVC app is typically placed in the
Pages (or Views) folder. A _ViewImports.cshtml file can be placed within any folder, in
which case it will only be applied to pages or views within that folder and its subfolders.
_ViewImports files are processed starting at the root level and then for each folder
leading up to the location of the page or view itself. _ViewImports settings specified at
the root level may be overridden at the folder level.

For example, suppose:

The root level _ViewImports.cshtml file includes @model MyModel1 and


@addTagHelper *, MyTagHelper1 .

A subfolder _ViewImports.cshtml file includes @model MyModel2 and @addTagHelper


*, MyTagHelper2 .

Pages and views in the subfolder will have access to both Tag Helpers and the MyModel2
model.

If multiple _ViewImports.cshtml files are found in the file hierarchy, the combined
behavior of the directives are:

@addTagHelper , @removeTagHelper : all run, in order


@tagHelperPrefix : the closest one to the view overrides any others

@model : the closest one to the view overrides any others


@inherits : the closest one to the view overrides any others

@using : all are included; duplicates are ignored

@inject : for each property, the closest one to the view overrides any others with
the same property name

Running Code Before Each View


Code that needs to run before each view or page should be placed in the
_ViewStart.cshtml file. By convention, the _ViewStart.cshtml file is located in the Pages
(or Views) folder. The statements listed in _ViewStart.cshtml are run before every full
view (not layouts, and not partial views). Like ViewImports.cshtml, _ViewStart.cshtml is
hierarchical. If a _ViewStart.cshtml file is defined in the view or pages folder, it will be
run after the one defined in the root of the Pages (or Views) folder (if any).

A sample _ViewStart.cshtml file:

CSHTML

@{
Layout = "_Layout";
}

The file above specifies that all views will use the _Layout.cshtml layout.
_ViewStart.cshtml and _ViewImports.cshtml are not typically placed in the

/Pages/Shared (or /Views/Shared) folder. The app-level versions of these files should be
placed directly in the /Pages (or /Views) folder.
Razor syntax reference for ASP.NET Core
Article • 02/23/2023

By Rick Anderson , Taylor Mullen , and Dan Vicarel

Razor is a markup syntax for embedding .NET based code into webpages. The Razor
syntax consists of Razor markup, C#, and HTML. Files containing Razor generally have a
.cshtml file extension. Razor is also found in Razor component files ( .razor ). Razor

syntax is similar to the templating engines of various JavaScript single-page application


(SPA) frameworks, such as Angular, React, VueJs, and Svelte. For more information see,
The features described in this article are obsolete as of ASP.NET Core 3.0.

Introduction to ASP.NET Web Programming Using the Razor Syntax provides many
samples of programming with Razor syntax. Although the topic was written for ASP.NET
rather than ASP.NET Core, most of the samples apply to ASP.NET Core.

Rendering HTML
The default Razor language is HTML. Rendering HTML from Razor markup is no different
than rendering HTML from an HTML file. HTML markup in .cshtml Razor files is
rendered by the server unchanged.

Razor syntax
Razor supports C# and uses the @ symbol to transition from HTML to C#. Razor
evaluates C# expressions and renders them in the HTML output.

When an @ symbol is followed by a Razor reserved keyword, it transitions into Razor-


specific markup. Otherwise, it transitions into plain HTML.

To escape an @ symbol in Razor markup, use a second @ symbol:

CSHTML

<p>@@Username</p>

The code is rendered in HTML with a single @ symbol:

HTML

<p>@Username</p>
HTML attributes and content containing email addresses don't treat the @ symbol as a
transition character. The email addresses in the following example are untouched by
Razor parsing:

CSHTML

<a href="mailto:Support@contoso.com">Support@contoso.com</a>

Scalable Vector Graphics (SVG)


SVG foreignObject elements are supported:

HTML

@{
string message = "foreignObject example with Scalable Vector Graphics
(SVG)";
}

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">


<rect x="0" y="0" rx="10" ry="10" width="200" height="200"
stroke="black"
fill="none" />
<foreignObject x="20" y="20" width="160" height="160">
<p>@message</p>
</foreignObject>
</svg>

Implicit Razor expressions


Implicit Razor expressions start with @ followed by C# code:

CSHTML

<p>@DateTime.Now</p>
<p>@DateTime.IsLeapYear(2016)</p>

With the exception of the C# await keyword, implicit expressions must not contain
spaces. If the C# statement has a clear ending, spaces can be intermingled:

CSHTML

<p>@await DoSomething("hello", "world")</p>


Implicit expressions cannot contain C# generics, as the characters inside the brackets
( <> ) are interpreted as an HTML tag. The following code is not valid:

CSHTML

<p>@GenericMethod<int>()</p>

The preceding code generates a compiler error similar to one of the following:

The "int" element wasn't closed. All elements must be either self-closing or have a
matching end tag.
Cannot convert method group 'GenericMethod' to non-delegate type 'object'. Did
you intend to invoke the method?`

Generic method calls must be wrapped in an explicit Razor expression or a Razor code
block.

Explicit Razor expressions


Explicit Razor expressions consist of an @ symbol with balanced parenthesis. To render
last week's time, the following Razor markup is used:

CSHTML

<p>Last week this time: @(DateTime.Now - TimeSpan.FromDays(7))</p>

Any content within the @() parenthesis is evaluated and rendered to the output.

Implicit expressions, described in the previous section, generally can't contain spaces. In
the following code, one week isn't subtracted from the current time:

CSHTML

<p>Last week: @DateTime.Now - TimeSpan.FromDays(7)</p>

The code renders the following HTML:

HTML

<p>Last week: 7/7/2016 4:39:52 PM - TimeSpan.FromDays(7)</p>

Explicit expressions can be used to concatenate text with an expression result:


CSHTML

@{
var joe = new Person("Joe", 33);
}

<p>Age@(joe.Age)</p>

Without the explicit expression, <p>Age@joe.Age</p> is treated as an email address, and


<p>Age@joe.Age</p> is rendered. When written as an explicit expression, <p>Age33</p> is

rendered.

Explicit expressions can be used to render output from generic methods in .cshtml files.
The following markup shows how to correct the error shown earlier caused by the
brackets of a C# generic. The code is written as an explicit expression:

CSHTML

<p>@(GenericMethod<int>())</p>

Expression encoding
C# expressions that evaluate to a string are HTML encoded. C# expressions that
evaluate to IHtmlContent are rendered directly through IHtmlContent.WriteTo . C#
expressions that don't evaluate to IHtmlContent are converted to a string by ToString
and encoded before they're rendered.

CSHTML

@("<span>Hello World</span>")

The preceding code renders the following HTML:

HTML

&lt;span&gt;Hello World&lt;/span&gt;

The HTML is shown in the browser as plain text:

<span>Hello World</span>

HtmlHelper.Raw output isn't encoded but rendered as HTML markup.


2 Warning

Using HtmlHelper.Raw on unsanitized user input is a security risk. User input might
contain malicious JavaScript or other exploits. Sanitizing user input is difficult. Avoid
using HtmlHelper.Raw with user input.

CSHTML

@Html.Raw("<span>Hello World</span>")

The code renders the following HTML:

HTML

<span>Hello World</span>

Razor code blocks


Razor code blocks start with @ and are enclosed by {} . Unlike expressions, C# code
inside code blocks isn't rendered. Code blocks and expressions in a view share the same
scope and are defined in order:

CSHTML

@{
var quote = "The future depends on what you do today. - Mahatma Gandhi";
}

<p>@quote</p>

@{
quote = "Hate cannot drive out hate, only love can do that. - Martin
Luther King, Jr.";
}

<p>@quote</p>

The code renders the following HTML:

HTML

<p>The future depends on what you do today. - Mahatma Gandhi</p>


<p>Hate cannot drive out hate, only love can do that. - Martin Luther King,
Jr.</p>
In code blocks, declare local functions with markup to serve as templating methods:

CSHTML

@{
void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}

RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}

The code renders the following HTML:

HTML

<p>Name: <strong>Mahatma Gandhi</strong></p>


<p>Name: <strong>Martin Luther King, Jr.</strong></p>

Implicit transitions
The default language in a code block is C#, but the Razor Page can transition back to
HTML:

CSHTML

@{
var inCSharp = true;
<p>Now in HTML, was in C# @inCSharp</p>
}

Explicit delimited transition


To define a subsection of a code block that should render HTML, surround the
characters for rendering with the Razor <text> tag:

CSHTML

@for (var i = 0; i < people.Length; i++)


{
var person = people[i];
<text>Name: @person.Name</text>
}
Use this approach to render HTML that isn't surrounded by an HTML tag. Without an
HTML or Razor tag, a Razor runtime error occurs.

The <text> tag is useful to control whitespace when rendering content:

Only the content between the <text> tag is rendered.


No whitespace before or after the <text> tag appears in the HTML output.

Explicit line transition


To render the rest of an entire line as HTML inside a code block, use @: syntax:

CSHTML

@for (var i = 0; i < people.Length; i++)


{
var person = people[i];
@:Name: @person.Name
}

Without the @: in the code, a Razor runtime error is generated.

Extra @ characters in a Razor file can cause compiler errors at statements later in the
block. These extra @ compiler errors:

Can be difficult to understand because the actual error occurs before the reported
error.
Is common after combining multiple implicit and explicit expressions into a single
code block.

Conditional attribute rendering


Razor automatically omits attributes that aren't needed. If the value passed in is null or
false , the attribute isn't rendered.

For example, consider the following razor:

CSHTML

<div class="@false">False</div>
<div class="@null">Null</div>
<div class="@("")">Empty</div>
<div class="@("false")">False String</div>
<div class="@("active")">String</div>
<input type="checkbox" checked="@true" name="true" />
<input type="checkbox" checked="@false" name="false" />
<input type="checkbox" checked="@null" name="null" />

The preceding Razor markup generates the following HTML:

HTML

<div>False</div>
<div>Null</div>
<div class="">Empty</div>
<div class="false">False String</div>
<div class="active">String</div>
<input type="checkbox" checked="checked" name="true">
<input type="checkbox" name="false">
<input type="checkbox" name="null">

Control structures
Control structures are an extension of code blocks. All aspects of code blocks
(transitioning to markup, inline C#) also apply to the following structures:

Conditionals @if, else if, else, and @switch


@if controls when code runs:

CSHTML

@if (value % 2 == 0)
{
<p>The value was even.</p>
}

else and else if don't require the @ symbol:

CSHTML

@if (value % 2 == 0)
{
<p>The value was even.</p>
}
else if (value >= 1337)
{
<p>The value is large.</p>
}
else
{
<p>The value is odd and small.</p>
}

The following markup shows how to use a switch statement:

CSHTML

@switch (value)
{
case 1:
<p>The value is 1!</p>
break;
case 1337:
<p>Your number is 1337!</p>
break;
default:
<p>Your number wasn't 1 or 1337.</p>
break;
}

Looping @for, @foreach, @while, and @do while


Templated HTML can be rendered with looping control statements. To render a list of
people:

CSHTML

@{
var people = new Person[]
{
new Person("Weston", 33),
new Person("Johnathon", 41),
...
};
}

The following looping statements are supported:

@for

CSHTML

@for (var i = 0; i < people.Length; i++)


{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
}
@foreach

CSHTML

@foreach (var person in people)


{
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
}

@while

CSHTML

@{ var i = 0; }
@while (i < people.Length)
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>

i++;
}

@do while

CSHTML

@{ var i = 0; }
@do
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>

i++;
} while (i < people.Length);

Compound @using
In C#, a using statement is used to ensure an object is disposed. In Razor, the same
mechanism is used to create HTML Helpers that contain additional content. In the
following code, HTML Helpers render a <form> tag with the @using statement:

CSHTML
@using (Html.BeginForm())
{
<div>
Email: <input type="email" id="Email" value="">
<button>Register</button>
</div>
}

@try, catch, finally

Exception handling is similar to C#:

CSHTML

@try
{
throw new InvalidOperationException("You did something invalid.");
}
catch (Exception ex)
{
<p>The exception message: @ex.Message</p>
}
finally
{
<p>The finally statement.</p>
}

@lock

Razor has the capability to protect critical sections with lock statements:

CSHTML

@lock (SomeLock)
{
// Do critical section work
}

Comments
Razor supports C# and HTML comments:

CSHTML
@{
/* C# comment */
// Another C# comment
}
<!-- HTML comment -->

The code renders the following HTML:

HTML

<!-- HTML comment -->

Razor comments are removed by the server before the webpage is rendered. Razor uses
@* *@ to delimit comments. The following code is commented out, so the server doesn't

render any markup:

CSHTML

@*
@{
/* C# comment */
// Another C# comment
}
<!-- HTML comment -->
*@

Directives
Razor directives are represented by implicit expressions with reserved keywords
following the @ symbol. A directive typically changes the way a view is parsed or
enables different functionality.

Understanding how Razor generates code for a view makes it easier to understand how
directives work.

CSHTML

@{
var quote = "Getting old ain't for wimps! - Anonymous";
}

<div>Quote of the Day: @quote</div>

The code generates a class similar to the following:


C#

public class _Views_Something_cshtml : RazorPage<dynamic>


{
public override async Task ExecuteAsync()
{
var output = "Getting old ain't for wimps! - Anonymous";

WriteLiteral("/r/n<div>Quote of the Day: ");


Write(output);
WriteLiteral("</div>");
}
}

Later in this article, the section Inspect the Razor C# class generated for a view explains
how to view this generated class.

@attribute

The @attribute directive adds the given attribute to the class of the generated page or
view. The following example adds the [Authorize] attribute:

CSHTML

@attribute [Authorize]

The @attribute directive can also be used to supply a constant-based route template in
a Razor component. In the following example, the @page directive in a component is
replaced with the @attribute directive and the constant-based route template in
Constants.CounterRoute , which is set elsewhere in the app to " /counter ":

diff

- @page "/counter"
+ @attribute [Route(Constants.CounterRoute)]

@code

This scenario only applies to Razor components ( .razor ).

The @code block enables a Razor component to add C# members (fields, properties, and
methods) to a component:

razor
@code {
// C# members (fields, properties, and methods)
}

For Razor components, @code is an alias of @functions and recommended over


@functions . More than one @code block is permissible.

@functions

The @functions directive enables adding C# members (fields, properties, and methods)
to the generated class:

CSHTML

@functions {
// C# members (fields, properties, and methods)
}

In Razor components, use @code over @functions to add C# members.

For example:

CSHTML

@functions {
public string GetHello()
{
return "Hello";
}
}

<div>From method: @GetHello()</div>

The code generates the following HTML markup:

HTML

<div>From method: Hello</div>

The following code is the generated Razor C# class:

C#

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor;
public class _Views_Home_Test_cshtml : RazorPage<dynamic>
{
// Functions placed between here
public string GetHello()
{
return "Hello";
}
// And here.
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
WriteLiteral("\r\n<div>From method: ");
Write(GetHello());
WriteLiteral("</div>\r\n");
}
#pragma warning restore 1998

@functions methods serve as templating methods when they have markup:

CSHTML

@{
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}

@functions {
private void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}
}

The code renders the following HTML:

HTML

<p>Name: <strong>Mahatma Gandhi</strong></p>


<p>Name: <strong>Martin Luther King, Jr.</strong></p>

@implements

The @implements directive implements an interface for the generated class.

The following example implements System.IDisposable so that the Dispose method can
be called:
CSHTML

@implements IDisposable

<h1>Example</h1>

@functions {
private bool _isDisposed;

...

public void Dispose() => _isDisposed = true;


}

@inherits

The @inherits directive provides full control of the class the view inherits:

CSHTML

@inherits TypeNameOfClassToInheritFrom

The following code is a custom Razor page type:

C#

using Microsoft.AspNetCore.Mvc.Razor;

public abstract class CustomRazorPage<TModel> : RazorPage<TModel>


{
public string CustomText { get; } =
"Gardyloo! - A Scottish warning yelled from a window before dumping"
+
"a slop bucket on the street below.";
}

The CustomText is displayed in a view:

CSHTML

@inherits CustomRazorPage<TModel>

<div>Custom text: @CustomText</div>

The code renders the following HTML:

HTML
<div>
Custom text: Gardyloo! - A Scottish warning yelled from a window before
dumping
a slop bucket on the street below.
</div>

@model and @inherits can be used in the same view. @inherits can be in a
_ViewImports.cshtml file that the view imports:

CSHTML

@inherits CustomRazorPage<TModel>

The following code is an example of a strongly-typed view:

CSHTML

@inherits CustomRazorPage<TModel>

<div>The Login Email: @Model.Email</div>


<div>Custom text: @CustomText</div>

If "rick@contoso.com" is passed in the model, the view generates the following HTML
markup:

HTML

<div>The Login Email: rick@contoso.com</div>


<div>
Custom text: Gardyloo! - A Scottish warning yelled from a window before
dumping
a slop bucket on the street below.
</div>

@inject

The @inject directive enables the Razor Page to inject a service from the service
container into a view. For more information, see Dependency injection into views.

@layout

This scenario only applies to Razor components ( .razor ).


The @layout directive specifies a layout for routable Razor components that have an
@page directive. Layout components are used to avoid code duplication and
inconsistency. For more information, see ASP.NET Core Blazor layouts.

@model

This scenario only applies to MVC views and Razor Pages ( .cshtml ).

The @model directive specifies the type of the model passed to a view or page:

CSHTML

@model TypeNameOfModel

In an ASP.NET Core MVC or Razor Pages app created with individual user accounts,
Views/Account/Login.cshtml contains the following model declaration:

CSHTML

@model LoginViewModel

The class generated inherits from RazorPage<LoginViewModel> :

C#

public class _Views_Account_Login_cshtml : RazorPage<LoginViewModel>

Razor exposes a Model property for accessing the model passed to the view:

CSHTML

<div>The Login Email: @Model.Email</div>

The @model directive specifies the type of the Model property. The directive specifies the
T in RazorPage<T> that the generated class that the view derives from. If the @model

directive isn't specified, the Model property is of type dynamic . For more information,
see Strongly typed models and the @model keyword.

@namespace

The @namespace directive:


Sets the namespace of the class of the generated Razor page, MVC view, or Razor
component.
Sets the root derived namespaces of a pages, views, or components classes from
the closest imports file in the directory tree, _ViewImports.cshtml (views or pages)
or _Imports.razor (Razor components).

CSHTML

@namespace Your.Namespace.Here

For the Razor Pages example shown in the following table:

Each page imports Pages/_ViewImports.cshtml .


Pages/_ViewImports.cshtml contains @namespace Hello.World .

Each page has Hello.World as the root of it's namespace.

Page Namespace

Pages/Index.cshtml Hello.World

Pages/MorePages/Page.cshtml Hello.World.MorePages

Pages/MorePages/EvenMorePages/Page.cshtml Hello.World.MorePages.EvenMorePages

The preceding relationships apply to import files used with MVC views and Razor
components.

When multiple import files have a @namespace directive, the file closest to the page,
view, or component in the directory tree is used to set the root namespace.

If the EvenMorePages folder in the preceding example has an imports file with @namespace
Another.Planet (or the Pages/MorePages/EvenMorePages/Page.cshtml file contains

@namespace Another.Planet ), the result is shown in the following table.

Page Namespace

Pages/Index.cshtml Hello.World

Pages/MorePages/Page.cshtml Hello.World.MorePages

Pages/MorePages/EvenMorePages/Page.cshtml Another.Planet

@page
The @page directive has different effects depending on the type of the file where it
appears. The directive:

In a .cshtml file indicates that the file is a Razor Page. For more information, see
Custom routes and Introduction to Razor Pages in ASP.NET Core.
Specifies that a Razor component should handle requests directly. For more
information, see ASP.NET Core Blazor routing and navigation.

@preservewhitespace

This scenario only applies to Razor components ( .razor ).

When set to false (default), whitespace in the rendered markup from Razor
components ( .razor ) is removed if:

Leading or trailing within an element.


Leading or trailing within a RenderFragment parameter. For example, child content
passed to another component.
It precedes or follows a C# code block, such as @if or @foreach .

@section

This scenario only applies to MVC views and Razor Pages ( .cshtml ).

The @section directive is used in conjunction with MVC and Razor Pages layouts to
enable views or pages to render content in different parts of the HTML page. For more
information, see Layout in ASP.NET Core.

@using

The @using directive adds the C# using directive to the generated view:

CSHTML

@using System.IO
@{
var dir = Directory.GetCurrentDirectory();
}
<p>@dir</p>

In Razor components, @using also controls which components are in scope.


Directive attributes
Razor directive attributes are represented by implicit expressions with reserved
keywords following the @ symbol. A directive attribute typically changes the way an
element is parsed or enables different functionality.

@attributes

This scenario only applies to Razor components ( .razor ).

@attributes allows a component to render non-declared attributes. For more

information, see ASP.NET Core Razor components.

@bind

This scenario only applies to Razor components ( .razor ).

Data binding in components is accomplished with the @bind attribute. For more
information, see ASP.NET Core Blazor data binding.

@bind:culture

This scenario only applies to Razor components ( .razor ).

Use the @bind:culture attribute with the @bind attribute to provide a


System.Globalization.CultureInfo for parsing and formatting a value. For more
information, see ASP.NET Core Blazor globalization and localization.

@on{EVENT}

This scenario only applies to Razor components ( .razor ).

Razor provides event handling features for components. For more information, see
ASP.NET Core Blazor event handling.

@on{EVENT}:preventDefault

This scenario only applies to Razor components ( .razor ).

Prevents the default action for the event.


@on{EVENT}:stopPropagation

This scenario only applies to Razor components ( .razor ).

Stops event propagation for the event.

@key

This scenario only applies to Razor components ( .razor ).

The @key directive attribute causes the components diffing algorithm to guarantee
preservation of elements or components based on the key's value. For more
information, see ASP.NET Core Razor components.

@ref

This scenario only applies to Razor components ( .razor ).

Component references ( @ref ) provide a way to reference a component instance so that


you can issue commands to that instance. For more information, see ASP.NET Core
Razor components.

@typeparam

This scenario only applies to Razor components ( .razor ).

The @typeparam directive declares a generic type parameter for the generated
component class:

razor

@typeparam TEntity

Generic types with where type constraints are supported:

razor

@typeparam TEntity where TEntity : IEntity

For more information, see the following articles:

ASP.NET Core Razor components


ASP.NET Core Blazor templated components

Templated Razor delegates


Razor templates allow you to define a UI snippet with the following format:

CSHTML

@<tag>...</tag>

The following example illustrates how to specify a templated Razor delegate as a


Func<T,TResult>. The dynamic type is specified for the parameter of the method that
the delegate encapsulates. An object type is specified as the return value of the
delegate. The template is used with a List<T> of Pet that has a Name property.

C#

public class Pet


{
public string Name { get; set; }
}

CSHTML

@{
Func<dynamic, object> petTemplate = @<p>You have a pet named
<strong>@item.Name</strong>.</p>;

var pets = new List<Pet>


{
new Pet { Name = "Rin Tin Tin" },
new Pet { Name = "Mr. Bigglesworth" },
new Pet { Name = "K-9" }
};
}

The template is rendered with pets supplied by a foreach statement:

CSHTML

@foreach (var pet in pets)


{
@petTemplate(pet)
}
Rendered output:

HTML

<p>You have a pet named <strong>Rin Tin Tin</strong>.</p>


<p>You have a pet named <strong>Mr. Bigglesworth</strong>.</p>
<p>You have a pet named <strong>K-9</strong>.</p>

You can also supply an inline Razor template as an argument to a method. In the
following example, the Repeat method receives a Razor template. The method uses the
template to produce HTML content with repeats of items supplied from a list:

CSHTML

@using Microsoft.AspNetCore.Html

@functions {
public static IHtmlContent Repeat(IEnumerable<dynamic> items, int times,
Func<dynamic, IHtmlContent> template)
{
var html = new HtmlContentBuilder();

foreach (var item in items)


{
for (var i = 0; i < times; i++)
{
html.AppendHtml(template(item));
}
}

return html;
}
}

Using the list of pets from the prior example, the Repeat method is called with:

List<T> of Pet .
Number of times to repeat each pet.
Inline template to use for the list items of an unordered list.

CSHTML

<ul>
@Repeat(pets, 3, @<li>@item.Name</li>)
</ul>

Rendered output:
HTML

<ul>
<li>Rin Tin Tin</li>
<li>Rin Tin Tin</li>
<li>Rin Tin Tin</li>
<li>Mr. Bigglesworth</li>
<li>Mr. Bigglesworth</li>
<li>Mr. Bigglesworth</li>
<li>K-9</li>
<li>K-9</li>
<li>K-9</li>
</ul>

Tag Helpers
This scenario only applies to MVC views and Razor Pages ( .cshtml ).

There are three directives that pertain to Tag Helpers.

Directive Function

@addTagHelper Makes Tag Helpers available to a view.

@removeTagHelper Removes Tag Helpers previously added from a view.

@tagHelperPrefix Specifies a tag prefix to enable Tag Helper support and to make Tag Helper
usage explicit.

Razor reserved keywords

Razor keywords
page

namespace

functions
inherits

model
section

helper (Not currently supported by ASP.NET Core)

Razor keywords are escaped with @(Razor Keyword) (for example, @(functions) ).
C# Razor keywords
case
do

default

for
foreach

if
else

lock

switch
try

catch
finally

using

while

C# Razor keywords must be double-escaped with @(@C# Razor Keyword) (for example,
@(@case) ). The first @ escapes the Razor parser. The second @ escapes the C# parser.

Reserved keywords not used by Razor


class

Inspect the Razor C# class generated for a view


The Razor SDK handles compilation of Razor files. By default, the generated code files
aren't emitted. To enable emitting the code files, set the EmitCompilerGeneratedFiles
directive in the project file ( .csproj ) to true :

XML

<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

When building a 6.0 project ( net6.0 ) in the Debug build configuration, the Razor SDK
generates an obj/Debug/net6.0/generated/ directory in the project root. Its subdirectory
contains the emitted Razor page code files.
View lookups and case sensitivity
The Razor view engine performs case-sensitive lookups for views. However, the actual
lookup is determined by the underlying file system:

File based source:


On operating systems with case insensitive file systems (for example, Windows),
physical file provider lookups are case insensitive. For example, return
View("Test") results in matches for /Views/Home/Test.cshtml ,

/Views/home/test.cshtml , and any other casing variant.

On case-sensitive file systems (for example, Linux, OSX, and with


EmbeddedFileProvider ), lookups are case-sensitive. For example, return

View("Test") specifically matches /Views/Home/Test.cshtml .


Precompiled views: With ASP.NET Core 2.0 and later, looking up precompiled views
is case insensitive on all operating systems. The behavior is identical to physical file
provider's behavior on Windows. If two precompiled views differ only in case, the
result of lookup is non-deterministic.

Developers are encouraged to match the casing of file and directory names to the
casing of:

Area, controller, and action names.


Razor Pages.

Matching case ensures the deployments find their views regardless of the underlying file
system.

Imports used by Razor


The following imports are generated by the ASP.NET Core web templates to support
Razor Files:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
Additional resources
Introduction to ASP.NET Web Programming Using the Razor Syntax provides many
samples of programming with Razor syntax.
Create reusable UI using the Razor class
library project in ASP.NET Core
Article • 11/07/2022

By Rick Anderson

Razor views, pages, controllers, page models, Razor components, View components, and
data models can be built into a Razor class library (RCL). The RCL can be packaged and
reused. Applications can include the RCL and override the views and pages it contains.
When a view, partial view, or Razor Page is found in both the web app and the RCL, the
Razor markup ( .cshtml file) in the web app takes precedence.

For information on how to integrate npm and webpack into the build process for a
Razor Class Library, see Build client web assets for your Razor Class Library .

Create a class library containing Razor UI


Visual Studio

From Visual Studio select Create new a new project.


Select Razor Class Library > Next.
Name the library (for example, "RazorClassLib"), > Create. To avoid a file name
collision with the generated view library, ensure the library name doesn't end
in .Views .
Select Support pages and views if you need to support views. By default, only
Razor Pages are supported. Select Create.

The Razor class library (RCL) template defaults to Razor component development
by default. The Support pages and views option supports pages and views.

Add Razor files to the RCL.

The ASP.NET Core templates assume the RCL content is in the Areas folder. See RCL
Pages layout below to create an RCL that exposes content in ~/Pages rather than
~/Areas/Pages .

Reference RCL content


The RCL can be referenced by:
NuGet package. See Creating NuGet packages and dotnet add package and Create
and publish a NuGet package.
{ProjectName}.csproj . See dotnet-add reference.

Override views, partial views, and pages


When a view, partial view, or Razor Page is found in both the web app and the RCL, the
Razor markup ( .cshtml file) in the web app takes precedence. For example, add
WebApp1/Areas/MyFeature/Pages/Page1.cshtml to WebApp1, and Page1 in the WebApp1

will take precedence over Page1 in the RCL.

In the sample download, rename WebApp1/Areas/MyFeature2 to WebApp1/Areas/MyFeature


to test precedence.

Copy the RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml partial view


to WebApp1/Areas/MyFeature/Pages/Shared/_Message.cshtml . Update the markup to
indicate the new location. Build and run the app to verify the app's version of the partial
is being used.

If the RCL uses Razor Pages, enable the Razor Pages services and endpoints in the
hosting app:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();

RCL Pages layout


To reference RCL content as though it is part of the web app's Pages folder, create the
RCL project with the following file structure:

RazorUIClassLib/Pages

RazorUIClassLib/Pages/Shared

Suppose RazorUIClassLib/Pages/Shared contains two partial files: _Header.cshtml and


_Footer.cshtml . The <partial> tags could be added to _Layout.cshtml file:

CSHTML

<body>
<partial name="_Header">
@RenderBody()
<partial name="_Footer">
</body>

Add the _ViewStart.cshtml file to the RCL project's Pages folder to use the
_Layout.cshtml file from the host web app:

CSHTML

@{
Layout = "_Layout";
}

Create an RCL with static assets


An RCL may require companion static assets that can be referenced by either the RCL or
the consuming app of the RCL. ASP.NET Core allows creating RCLs that include static
assets that are available to a consuming app.

To include companion assets as part of an RCL, create a wwwroot folder in the class
library and include any required files in that folder.

When packing an RCL, all companion assets in the wwwroot folder are automatically
included in the package.
Use the dotnet pack command rather than the NuGet.exe version nuget pack .

Exclude static assets


To exclude static assets, add the desired exclusion path to the $(DefaultItemExcludes)
property group in the project file. Separate entries with a semicolon ( ; ).

In the following example, the lib.css stylesheet in the wwwroot folder isn't considered a
static asset and isn't included in the published RCL:

XML

<PropertyGroup>

<DefaultItemExcludes>$(DefaultItemExcludes);wwwroot\lib.css</DefaultItemExcl
udes>
</PropertyGroup>

Typescript integration
To include TypeScript files in an RCL:

1. Reference the Microsoft.TypeScript.MSBuild NuGet package in the project.

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 .

2. Place the TypeScript files ( .ts ) outside of the wwwroot folder. For example, place
the files in a Client folder.

3. Configure the TypeScript build output for the wwwroot folder. Set the
TypescriptOutDir property inside of a PropertyGroup in the project file:

XML

<TypescriptOutDir>wwwroot</TypescriptOutDir>

4. Include the TypeScript target as a dependency of the PrepareForBuildDependsOn


target by adding the following target inside of a PropertyGroup in the project file:
XML

<PrepareForBuildDependsOn>
CompileTypeScript;
GetTypeScriptOutputForPublishing;$(PrepareForBuildDependsOn)
</PrepareForBuildDependsOn>

Consume content from a referenced RCL


The files included in the wwwroot folder of the RCL are exposed to either the RCL or the
consuming app under the prefix _content/{PACKAGE ID}/ . For example, a library with an
assembly name of Razor.Class.Lib and without a <PackageId> specified in its project
file results in a path to static content at _content/Razor.Class.Lib/ . When producing a
NuGet package and the assembly name isn't the same as the package ID (<PackageId>
in the library's project file), use the package ID as specified in the project file for
{PACKAGE ID} .

The consuming app references static assets provided by the library with <script> ,
<style> , <img> , and other HTML tags. The consuming app must have static file support
enabled in:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();

When running the consuming app from build output ( dotnet run ), static web assets are
enabled by default in the Development environment. To support assets in other
environments when running from build output, call UseStaticWebAssets on the host
builder in Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Calling UseStaticWebAssets isn't required when running an app from published output
( dotnet publish ).

Multi-project development flow


When the consuming app runs:

The assets in the RCL stay in their original folders. The assets aren't moved to the
consuming app.
Any change within the RCL's wwwroot folder is reflected in the consuming app after
the RCL is rebuilt and without rebuilding the consuming app.
When the RCL is built, a manifest is produced that describes the static web asset
locations. The consuming app reads the manifest at runtime to consume the assets from
referenced projects and packages. When a new asset is added to an RCL, the RCL must
be rebuilt to update its manifest before a consuming app can access the new asset.

Publish
When the app is published, the companion assets from all referenced projects and
packages are copied into the wwwroot folder of the published app under
_content/{PACKAGE ID}/ . When producing a NuGet package and the assembly name

isn't the same as the package ID (<PackageId> in the library's project file), use the
package ID as specified in the project file for {PACKAGE ID} when examining the wwwroot
folder for the published assets.

Additional resources
View or download sample code (how to download)

Consume ASP.NET Core Razor components from a Razor class library (RCL)

ASP.NET Core Blazor CSS isolation


ASP.NET Core built-in Tag Helpers
Article • 06/03/2022

By Peter Kellner

For an overview of Tag Helpers, see Tag Helpers in ASP.NET Core.

There are built-in Tag Helpers which aren't listed in this document. The unlisted Tag
Helpers are used internally by the Razor view engine. The Tag Helper for the ~ (tilde)
character is unlisted. The tilde Tag Helper expands to the root path of the website.

Built-in ASP.NET Cores


Anchor

Cache

Component

Distributed Cache

Environment

Form

Form Action

Image

Input

Label

Link

Partial

Persist Component State

Script

Select

Textarea

Validation Message
Validation Summary

Additional resources
Tag Helpers in ASP.NET Core
Tag Helper Components in ASP.NET Core
Tag Helpers in ASP.NET Core
Article • 09/12/2022

By Rick Anderson

What are Tag Helpers


Tag Helpers enable server-side code to participate in creating and rendering HTML
elements in Razor files. For example, the built-in ImageTagHelper can append a version
number to the image name. Whenever the image changes, the server generates a new
unique version for the image, so clients are guaranteed to get the current image
(instead of a stale cached image). There are many built-in Tag Helpers for common tasks
- such as creating forms, links, loading assets and more - and even more available in
public GitHub repositories and as NuGet packages. Tag Helpers are authored in C#, and
they target HTML elements based on element name, attribute name, or parent tag. For
example, the built-in LabelTagHelper can target the HTML <label> element when the
LabelTagHelper attributes are applied. If you're familiar with HTML Helpers , Tag
Helpers reduce the explicit transitions between HTML and C# in Razor views. In many
cases, HTML Helpers provide an alternative approach to a specific Tag Helper, but it's
important to recognize that Tag Helpers don't replace HTML Helpers and there's not a
Tag Helper for each HTML Helper. Tag Helpers compared to HTML Helpers explains the
differences in more detail.

Tag Helpers aren't supported in Razor components. For more information, see ASP.NET
Core Razor components.

What Tag Helpers provide


An HTML-friendly development experience

For the most part, Razor markup using Tag Helpers looks like standard HTML. Front-end
designers conversant with HTML/CSS/JavaScript can edit Razor without learning C#
Razor syntax.

A rich IntelliSense environment for creating HTML and Razor markup

This is in sharp contrast to HTML Helpers, the previous approach to server-side creation
of markup in Razor views. Tag Helpers compared to HTML Helpers explains the
differences in more detail. IntelliSense support for Tag Helpers explains the IntelliSense
environment. Even developers experienced with Razor C# syntax are more productive
using Tag Helpers than writing C# Razor markup.

A way to make you more productive and able to produce more robust, reliable, and
maintainable code using information only available on the server

For example, historically the mantra on updating images was to change the name of the
image when you change the image. Images should be aggressively cached for
performance reasons, and unless you change the name of an image, you risk clients
getting a stale copy. Historically, after an image was edited, the name had to be
changed and each reference to the image in the web app needed to be updated. Not
only is this very labor intensive, it's also error prone (you could miss a reference,
accidentally enter the wrong string, etc.) The built-in ImageTagHelper can do this for you
automatically. The ImageTagHelper can append a version number to the image name, so
whenever the image changes, the server automatically generates a new unique version
for the image. Clients are guaranteed to get the current image. This robustness and
labor savings comes essentially free by using the ImageTagHelper .

Most built-in Tag Helpers target standard HTML elements and provide server-side
attributes for the element. For example, the <input> element used in many views in the
Views/Account folder contains the asp-for attribute. This attribute extracts the name of
the specified model property into the rendered HTML. Consider a Razor view with the
following model:

C#

public class Movie


{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}

The following Razor markup:

CSHTML

<label asp-for="Movie.Title"></label>

Generates the following HTML:

HTML
<label for="Movie_Title">Title</label>

The asp-for attribute is made available by the For property in the LabelTagHelper. See
Author Tag Helpers for more information.

Managing Tag Helper scope


Tag Helpers scope is controlled by a combination of @addTagHelper , @removeTagHelper ,
and the "!" opt-out character.

@addTagHelper makes Tag Helpers available

If you create a new ASP.NET Core web app named AuthoringTagHelpers, the following
Views/_ViewImports.cshtml file will be added to your project:

CSHTML

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers

The @addTagHelper directive makes Tag Helpers available to the view. In this case, the
view file is Pages/_ViewImports.cshtml , which by default is inherited by all files in the
Pages folder and subfolders; making Tag Helpers available. The code above uses the
wildcard syntax ("*") to specify that all Tag Helpers in the specified assembly
(Microsoft.AspNetCore.Mvc.TagHelpers) will be available to every view file in the Views
directory or subdirectory. The first parameter after @addTagHelper specifies the Tag
Helpers to load (we are using "*" for all Tag Helpers), and the second parameter
"Microsoft.AspNetCore.Mvc.TagHelpers" specifies the assembly containing the Tag
Helpers. Microsoft.AspNetCore.Mvc.TagHelpers is the assembly for the built-in ASP.NET
Core Tag Helpers.

To expose all of the Tag Helpers in this project (which creates an assembly named
AuthoringTagHelpers), you would use the following:

CSHTML

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers
If your project contains an EmailTagHelper with the default namespace
( AuthoringTagHelpers.TagHelpers.EmailTagHelper ), you can provide the fully qualified
name (FQN) of the Tag Helper:

CSHTML

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper,
AuthoringTagHelpers

To add a Tag Helper to a view using an FQN, you first add the FQN
( AuthoringTagHelpers.TagHelpers.EmailTagHelper ), and then the assembly name
(AuthoringTagHelpers). Most developers prefer to use the "*" wildcard syntax. The
wildcard syntax allows you to insert the wildcard character "*" as the suffix in an FQN.
For example, any of the following directives will bring in the EmailTagHelper :

CSHTML

@addTagHelper AuthoringTagHelpers.TagHelpers.E*, AuthoringTagHelpers


@addTagHelper AuthoringTagHelpers.TagHelpers.Email*, AuthoringTagHelpers

As mentioned previously, adding the @addTagHelper directive to the


Views/_ViewImports.cshtml file makes the Tag Helper available to all view files in the

Views directory and subdirectories. You can use the @addTagHelper directive in specific
view files if you want to opt-in to exposing the Tag Helper to only those views.

@removeTagHelper removes Tag Helpers

The @removeTagHelper has the same two parameters as @addTagHelper , and it removes a
Tag Helper that was previously added. For example, @removeTagHelper applied to a
specific view removes the specified Tag Helper from the view. Using @removeTagHelper in
a Views/Folder/_ViewImports.cshtml file removes the specified Tag Helper from all of
the views in Folder.

Controlling Tag Helper scope with the


_ViewImports.cshtml file

You can add a _ViewImports.cshtml to any view folder, and the view engine applies the
directives from both that file and the Views/_ViewImports.cshtml file. If you added an
empty Views/Home/_ViewImports.cshtml file for the Home views, there would be no
change because the _ViewImports.cshtml file is additive. Any @addTagHelper directives
you add to the Views/Home/_ViewImports.cshtml file (that are not in the default
Views/_ViewImports.cshtml file) would expose those Tag Helpers to views only in the

Home folder.

Opting out of individual elements


You can disable a Tag Helper at the element level with the Tag Helper opt-out character
("!"). For example, Email validation is disabled in the <span> with the Tag Helper opt-out
character:

CSHTML

<!span asp-validation-for="Email" class="text-danger"></!span>

You must apply the Tag Helper opt-out character to the opening and closing tag. (The
Visual Studio editor automatically adds the opt-out character to the closing tag when
you add one to the opening tag). After you add the opt-out character, the element and
Tag Helper attributes are no longer displayed in a distinctive font.

Using @tagHelperPrefix to make Tag Helper usage explicit


The @tagHelperPrefix directive allows you to specify a tag prefix string to enable Tag
Helper support and to make Tag Helper usage explicit. For example, you could add the
following markup to the Views/_ViewImports.cshtml file:

CSHTML

@tagHelperPrefix th:

In the code image below, the Tag Helper prefix is set to th: , so only those elements
using the prefix th: support Tag Helpers (Tag Helper-enabled elements have a
distinctive font). The <label> and <input> elements have the Tag Helper prefix and are
Tag Helper-enabled, while the <span> element doesn't.
The same hierarchy rules that apply to @addTagHelper also apply to @tagHelperPrefix .

Self-closing Tag Helpers


Many Tag Helpers can't be used as self-closing tags. Some Tag Helpers are designed to
be self-closing tags. Using a Tag Helper that was not designed to be self-closing
suppresses the rendered output. Self-closing a Tag Helper results in a self-closing tag in
the rendered output. For more information, see this note in Authoring Tag Helpers.

C# in Tag Helpers attribute/declaration


Tag Helpers do not allow C# in the element's attribute or tag declaration area. For
example, the following code is not valid:

CSHTML

<input asp-for="LastName"
@(Model?.LicenseId == null ? "disabled" : string.Empty) />

The preceding code can be written as:

CSHTML

<input asp-for="LastName"
disabled="@(Model?.LicenseId == null)" />

Normally, the @ operator inserts a textual representation of an expression into the


rendered HTML markup. However, when an expression evaluates to logical false , the
framework removes the attribute instead. In the preceding example, the disabled
attribute is set to true if either Model or LicenseId is null .

Tag helper initializers


While attributes can be used to configure individual instances of tag helpers,
ITagHelperInitializer<TTagHelper> can be used to configure all tag helper instances of a
specific kind. Consider the following example of a tag helper initializer that configures
the asp-append-version attribute or AppendVersion property for all instances of
ScriptTagHelper in the app:

C#
public class AppendVersionTagHelperInitializer :
ITagHelperInitializer<ScriptTagHelper>
{
public void Initialize(ScriptTagHelper helper, ViewContext context)
{
helper.AppendVersion = true;
}
}

To use the initializer, configure it by registering it as part of the application's startup:

C#

builder.Services.AddSingleton
<ITagHelperInitializer<ScriptTagHelper>,
AppendVersionTagHelperInitializer>();

Tag Helper automatic version generation


outside of wwwroot
For a Tag Helper to generate a version for a static file outside wwwroot , see Serve files
from multiple locations

IntelliSense support for Tag Helpers


Consider writing an HTML <label> element. As soon as you enter <l in the Visual
Studio editor, IntelliSense displays matching elements:

Not only do you get HTML help, but also the icon (the "@" symbol with "<>" under it).
The icon identifies the element as targeted by Tag Helpers. Pure HTML elements (such
as the fieldset ) display the "<>" icon.

A pure HTML <label> tag displays the HTML tag (with the default Visual Studio color
theme) in a brown font, the attributes in red, and the attribute values in blue.

After you enter <label , IntelliSense lists the available HTML/CSS attributes and the Tag
Helper-targeted attributes:

IntelliSense statement completion allows you to enter the tab key to complete the
statement with the selected value:

As soon as a Tag Helper attribute is entered, the tag and attribute fonts change. Using
the default Visual Studio "Blue" or "Light" color theme, the font is bold purple. If you're
using the "Dark" theme the font is bold teal. The images in this document were taken
using the default theme.

You can enter the Visual Studio CompleteWord shortcut (Ctrl +spacebar is the default)
inside the double quotes (""), and you are now in C#, just like you would be in a C#
class. IntelliSense displays all the methods and properties on the page model. The
methods and properties are available because the property type is ModelExpression . In
the image below, I'm editing the Register view, so the RegisterViewModel is available.
IntelliSense lists the properties and methods available to the model on the page. The
rich IntelliSense environment helps you select the CSS class:

Tag Helpers compared to HTML Helpers


Tag Helpers attach to HTML elements in Razor views, while HTML Helpers are invoked
as methods interspersed with HTML in Razor views. Consider the following Razor
markup, which creates an HTML label with the CSS class "caption":

CSHTML

@Html.Label("FirstName", "First Name:", new {@class="caption"})

The at ( @ ) symbol tells Razor this is the start of code. The next two parameters
("FirstName" and "First Name:") are strings, so IntelliSense can't help. The last argument:

CSHTML

new {@class="caption"}
Is an anonymous object used to represent attributes. Because class is a reserved
keyword in C#, you use the @ symbol to force C# to interpret @class= as a symbol
(property name). To a front-end designer (someone familiar with HTML/CSS/JavaScript
and other client technologies but not familiar with C# and Razor), most of the line is
foreign. The entire line must be authored with no help from IntelliSense.

Using the LabelTagHelper , the same markup can be written as:

CSHTML

<label class="caption" asp-for="FirstName"></label>

With the Tag Helper version, as soon as you enter <l in the Visual Studio editor,
IntelliSense displays matching elements:

IntelliSense helps you write the entire line.

The following code image shows the Form portion of the


Views/Account/Register.cshtml Razor view generated from the ASP.NET 4.5.x MVC
template included with Visual Studio.
The Visual Studio editor displays C# code with a grey background. For example, the
AntiForgeryToken HTML Helper:

CSHTML

@Html.AntiForgeryToken()

is displayed with a grey background. Most of the markup in the Register view is C#.
Compare that to the equivalent approach using Tag Helpers:
The markup is much cleaner and easier to read, edit, and maintain than the HTML
Helpers approach. The C# code is reduced to the minimum that the server needs to
know about. The Visual Studio editor displays markup targeted by a Tag Helper in a
distinctive font.

Consider the Email group:

CSHTML

<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>

Each of the "asp-" attributes has a value of "Email", but "Email" isn't a string. In this
context, "Email" is the C# model expression property for the RegisterViewModel .

The Visual Studio editor helps you write all of the markup in the Tag Helper approach of
the register form, while Visual Studio provides no help for most of the code in the HTML
Helpers approach. IntelliSense support for Tag Helpers goes into detail on working with
Tag Helpers in the Visual Studio editor.

Tag Helpers compared to Web Server Controls


Tag Helpers don't own the element they're associated with; they simply participate
in the rendering of the element and content. ASP.NET Web Server Controls are
declared and invoked on a page.

ASP.NET Web Server Controls have a non-trivial lifecycle that can make developing
and debugging difficult.

Web Server controls allow you to add functionality to the client Document Object
Model (DOM) elements by using a client control. Tag Helpers have no DOM.

Web Server controls include automatic browser detection. Tag Helpers have no
knowledge of the browser.

Multiple Tag Helpers can act on the same element (see Avoiding Tag Helper
conflicts) while you typically can't compose Web Server controls.

Tag Helpers can modify the tag and content of HTML elements that they're scoped
to, but don't directly modify anything else on a page. Web Server controls have a
less specific scope and can perform actions that affect other parts of your page;
enabling unintended side effects.

Web Server controls use type converters to convert strings into objects. With Tag
Helpers, you work natively in C#, so you don't need to do type conversion.

Web Server controls use System.ComponentModel to implement the run-time and


design-time behavior of components and controls. System.ComponentModel
includes the base classes and interfaces for implementing attributes and type
converters, binding to data sources, and licensing components. Contrast that to
Tag Helpers, which typically derive from TagHelper , and the TagHelper base class
exposes only two methods, Process and ProcessAsync .

Customizing the Tag Helper element font


You can customize the font and colorization from Tools > Options > Environment >
Fonts and Colors:
Built-in ASP.NET Cores
Anchor

Cache

Component

Distributed Cache

Environment

Form

Form Action

Image

Input

Label
Link

Partial

Persist Component State

Script

Select

Textarea

Validation Message

Validation Summary

Additional resources
Author Tag Helpers
Working with Forms
TagHelperSamples on GitHub contains Tag Helper samples for working with
Bootstrap .
Author Tag Helpers in ASP.NET Core
Article • 09/14/2022

By Rick Anderson

View or download sample code (how to download)

Get started with Tag Helpers


This tutorial provides an introduction to programming Tag Helpers. Introduction to Tag
Helpers describes the benefits that Tag Helpers provide.

A tag helper is any class that implements the ITagHelper interface. However, when you
author a tag helper, you generally derive from TagHelper , doing so gives you access to
the Process method.

1. Create a new ASP.NET Core project called AuthoringTagHelpers. You won't need
authentication for this project.

2. Create a folder to hold the Tag Helpers called TagHelpers. The TagHelpers folder is
not required, but it's a reasonable convention. Now let's get started writing some
simple tag helpers.

A minimal Tag Helper


In this section, you write a tag helper that updates an email tag. For example:

HTML

<email>Support</email>

The server will use our email tag helper to convert that markup into the following:

HTML

<a href="mailto:Support@contoso.com">Support@contoso.com</a>

That is, an anchor tag that makes this an email link. You might want to do this if you are
writing a blog engine and need it to send email for marketing, support, and other
contacts, all to the same domain.
1. Add the following EmailTagHelper class to the TagHelpers folder.

C#

using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;

namespace AuthoringTagHelpers.TagHelpers
{
public class EmailTagHelper : TagHelper
{
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag
}
}
}

Tag helpers use a naming convention that targets elements of the root class
name (minus the TagHelper portion of the class name). In this example, the
root name of EmailTagHelper is email, so the <email> tag will be targeted.
This naming convention should work for most tag helpers, later on I'll show
how to override it.

The EmailTagHelper class derives from TagHelper . The TagHelper class


provides methods and properties for writing Tag Helpers.

The overridden Process method controls what the tag helper does when
executed. The TagHelper class also provides an asynchronous version
( ProcessAsync ) with the same parameters.

The context parameter to Process (and ProcessAsync ) contains information


associated with the execution of the current HTML tag.

The output parameter to Process (and ProcessAsync ) contains a stateful


HTML element representative of the original source used to generate an
HTML tag and content.

Our class name has a suffix of TagHelper, which is not required, but it's
considered a best practice convention. You could declare the class as:

C#

public class Email : TagHelper


2. To make the EmailTagHelper class available to all our Razor views, add the
addTagHelper directive to the Views/_ViewImports.cshtml file:

CSHTML

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers

The code above uses the wildcard syntax to specify all the tag helpers in our
assembly will be available. The first string after @addTagHelper specifies the tag
helper to load (Use "*" for all tag helpers), and the second string
"AuthoringTagHelpers" specifies the assembly the tag helper is in. Also, note that
the second line brings in the ASP.NET Core MVC tag helpers using the wildcard
syntax (those helpers are discussed in Introduction to Tag Helpers.) It's the
@addTagHelper directive that makes the tag helper available to the Razor view.
Alternatively, you can provide the fully qualified name (FQN) of a tag helper as
shown below:

C#

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper,
AuthoringTagHelpers

To add a tag helper to a view using a FQN, you first add the FQN
( AuthoringTagHelpers.TagHelpers.EmailTagHelper ), and then the assembly name
(AuthoringTagHelpers, not necessarily the namespace ). Most developers will prefer to use
the wildcard syntax. Introduction to Tag Helpers goes into detail on tag helper adding,
removing, hierarchy, and wildcard syntax.

1. Update the markup in the Views/Home/Contact.cshtml file with these changes:

CSHTML

@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>

2. Run the app and use your favorite browser to view the HTML source so you can
verify that the email tags are replaced with anchor markup (For example,
<a>Support</a> ). Support and Marketing are rendered as a links, but they don't

have an href attribute to make them functional. We'll fix that in the next section.

SetAttribute and SetContent


In this section, we'll update the EmailTagHelper so that it will create a valid anchor tag
for email. We'll update it to take information from a Razor view (in the form of a mail-
to attribute) and use that in generating the anchor.

Update the EmailTagHelper class with the following:

C#

public class EmailTagHelper : TagHelper


{
private const string EmailDomain = "contoso.com";

// Can be passed via <email mail-to="..." />.


// PascalCase gets translated into kebab-case.
public string MailTo { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput


output)
{
output.TagName = "a"; // Replaces <email> with <a> tag

var address = MailTo + "@" + EmailDomain;


output.Attributes.SetAttribute("href", "mailto:" + address);
output.Content.SetContent(address);
}
}

Pascal-cased class and property names for tag helpers are translated into their
kebab case . Therefore, to use the MailTo attribute, you'll use <email mail-
to="value"/> equivalent.
The last line sets the completed content for our minimally functional tag helper.

The highlighted line shows the syntax for adding attributes:

C#

public override void Process(TagHelperContext context, TagHelperOutput


output)
{
output.TagName = "a"; // Replaces <email> with <a> tag

var address = MailTo + "@" + EmailDomain;


output.Attributes.SetAttribute("href", "mailto:" + address);
output.Content.SetContent(address);
}

That approach works for the attribute "href" as long as it doesn't currently exist in the
attributes collection. You can also use the output.Attributes.Add method to add a tag
helper attribute to the end of the collection of tag attributes.

1. Update the markup in the Views/Home/Contact.cshtml file with these changes:

CSHTML

@{
ViewData["Title"] = "Contact Copy";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
One Microsoft Way Copy Version <br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong><email mail-to="Support"></email><br />
<strong>Marketing:</strong><email mail-to="Marketing"></email>
</address>

2. Run the app and verify that it generates the correct links.

7 Note

If you were to write the email tag self-closing ( <email mail-to="Rick" /> ), the final
output would also be self-closing. To enable the ability to write the tag with only a
start tag ( <email mail-to="Rick"> ) you must mark the class with the following:

C#

[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)]


public class EmailVoidTagHelper : TagHelper
{
private const string EmailDomain = "contoso.com";
// Code removed for brevity

With a self-closing email tag helper, the output would be <a


href="mailto:Rick@contoso.com" /> . Self-closing anchor tags are not valid HTML, so you

wouldn't want to create one, but you might want to create a tag helper that's self-
closing. Tag helpers set the type of the TagMode property after reading a tag.

You can also map a different attribute name to a property using the
[HtmlAttributeName] attribute.

To map an attribute named recipient to the MailTo property:

C#

[HtmlAttributeName("recipient")]
public string? MailTo { get; set; }

Tag Helper for the recipient attribute:

HTML

<email recipient="…"/>

ProcessAsync
In this section, we'll write an asynchronous email helper.

1. Replace the EmailTagHelper class with the following code:

C#

public class EmailTagHelper : TagHelper


{
private const string EmailDomain = "contoso.com";
public override async Task ProcessAsync(TagHelperContext context,
TagHelperOutput output)
{
output.TagName = "a"; //
Replaces <email> with <a> tag
var content = await output.GetChildContentAsync();
var target = content.GetContent() + "@" + EmailDomain;
output.Attributes.SetAttribute("href", "mailto:" + target);
output.Content.SetContent(target);
}
}

Notes:

This version uses the asynchronous ProcessAsync method. The asynchronous


GetChildContentAsync returns a Task containing the TagHelperContent .

Use the output parameter to get contents of the HTML element.

2. Make the following change to the Views/Home/Contact.cshtml file so the tag helper
can get the target email.

CSHTML

@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>

3. Run the app and verify that it generates valid email links.

RemoveAll, PreContent.SetHtmlContent and


PostContent.SetHtmlContent
1. Add the following BoldTagHelper class to the TagHelpers folder.

C#
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.SetHtmlContent("<strong>");
output.PostContent.SetHtmlContent("</strong>");
}
}
}

The [HtmlTargetElement] attribute passes an attribute parameter that


specifies that any HTML element that contains an HTML attribute named
"bold" will match, and the Process override method in the class will run. In
our sample, the Process method removes the "bold" attribute and surrounds
the containing markup with <strong></strong> .

Because you don't want to replace the existing tag content, you must write
the opening <strong> tag with the PreContent.SetHtmlContent method and
the closing </strong> tag with the PostContent.SetHtmlContent method.

2. Modify the About.cshtml view to contain a bold attribute value. The completed
code is shown below.

CSHTML

@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p bold>Use this area to provide additional information.</p>

<bold> Is this bold?</bold>

3. Run the app. You can use your favorite browser to inspect the source and verify the
markup.

The [HtmlTargetElement] attribute above only targets HTML markup that provides
an attribute name of "bold". The <bold> element wasn't modified by the tag
helper.

4. Comment out the [HtmlTargetElement] attribute line and it will default to targeting
<bold> tags, that is, HTML markup of the form <bold> . Remember, the default

naming convention will match the class name BoldTagHelper to <bold> tags.

5. Run the app and verify that the <bold> tag is processed by the tag helper.

Decorating a class with multiple [HtmlTargetElement] attributes results in a logical-OR


of the targets. For example, using the code below, a bold tag or a bold attribute will
match.

C#

[HtmlTargetElement("bold")]
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput
output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.SetHtmlContent("<strong>");
output.PostContent.SetHtmlContent("</strong>");
}
}

When multiple attributes are added to the same statement, the runtime treats them as a
logical-AND. For example, in the code below, an HTML element must be named "bold"
with an attribute named "bold" ( <bold bold /> ) to match.

C#

[HtmlTargetElement("bold", Attributes = "bold")]

You can also use the [HtmlTargetElement] to change the name of the targeted element.
For example if you wanted the BoldTagHelper to target <MyBold> tags, you would use
the following attribute:

C#

[HtmlTargetElement("MyBold")]

Pass a model to a Tag Helper


1. Add a Models folder.

2. Add the following WebsiteContext class to the Models folder:

C#

using System;

namespace AuthoringTagHelpers.Models
{
public class WebsiteContext
{
public Version Version { get; set; }
public int CopyrightYear { get; set; }
public bool Approved { get; set; }
public int TagsToShow { get; set; }
}
}

3. Add the following WebsiteInformationTagHelper class to the TagHelpers folder.

C#

using System;
using AuthoringTagHelpers.Models;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
public class WebsiteInformationTagHelper : TagHelper
{
public WebsiteContext Info { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
output.TagName = "section";
output.Content.SetHtmlContent(
$@"<ul><li><strong>Version:</strong> {Info.Version}</li>
<li><strong>Copyright Year:</strong> {Info.CopyrightYear}</li>
<li><strong>Approved:</strong> {Info.Approved}</li>
<li><strong>Number of tags to show:</strong> {Info.TagsToShow}</li>
</ul>");
output.TagMode = TagMode.StartTagAndEndTag;
}
}
}

As mentioned previously, tag helpers translates Pascal-cased C# class names


and properties for tag helpers into kebab case . Therefore, to use the
WebsiteInformationTagHelper in Razor, you'll write <website-information /> .

You are not explicitly identifying the target element with the
[HtmlTargetElement] attribute, so the default of website-information will be

targeted. If you applied the following attribute (note it's not kebab case but
matches the class name):

C#

[HtmlTargetElement("WebsiteInformation")]

The kebab case tag <website-information /> wouldn't match. If you want use the
[HtmlTargetElement] attribute, you would use kebab case as shown below:

C#

[HtmlTargetElement("Website-Information")]

Elements that are self-closing have no content. For this example, the Razor
markup will use a self-closing tag, but the tag helper will be creating a
section element (which isn't self-closing and you are writing content inside
the section element). Therefore, you need to set TagMode to
StartTagAndEndTag to write output. Alternatively, you can comment out the
line setting TagMode and write markup with a closing tag. (Example markup is
provided later in this tutorial.)

The $ (dollar sign) in the following line uses an interpolated string:

CSHTML

$@"<ul><li><strong>Version:</strong> {Info.Version}</li>

4. Add the following markup to the About.cshtml view. The highlighted markup
displays the web site information.

CSHTML

@using AuthoringTagHelpers.Models
@{
ViewData["Title"] = "About";
WebsiteContext webContext = new WebsiteContext {
Version = new Version(1, 3),
CopyrightYear = 1638,
Approved = true,
TagsToShow = 131 };
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p bold>Use this area to provide additional information.</p>

<bold> Is this bold?</bold>

<h3> web site info </h3>


<website-information info="webContext" />

7 Note

In the Razor markup shown below:

HTML

<website-information info="webContext" />

Razor knows the info attribute is a class, not a string, and you want to write
C# code. Any non-string tag helper attribute should be written without the @
character.

5. Run the app, and navigate to the About view to see the web site information.

7 Note

You can use the following markup with a closing tag and remove the line with
TagMode.StartTagAndEndTag in the tag helper:

HTML

<website-information info="webContext" >


</website-information>

Condition Tag Helper


The condition tag helper renders output when passed a true value.
1. Add the following ConditionTagHelper class to the TagHelpers folder.

C#

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = nameof(Condition))]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
if (!Condition)
{
output.SuppressOutput();
}
}
}
}

2. Replace the contents of the Views/Home/Index.cshtml file with the following


markup:

CSHTML

@using AuthoringTagHelpers.Models
@model WebsiteContext

@{
ViewData["Title"] = "Home Page";
}

<div>
<h3>Information about our website (outdated):</h3>
<Website-InforMation info="Model" />
<div condition="Model.Approved">
<p>
This website has <strong
surround="em">@Model.Approved</strong> been approved yet.
Visit www.contoso.com for more information.
</p>
</div>
</div>

3. Replace the Index method in the Home controller with the following code:
C#

public IActionResult Index(bool approved = false)


{
return View(new WebsiteContext
{
Approved = approved,
CopyrightYear = 2015,
Version = new Version(1, 3, 3, 7),
TagsToShow = 20
});
}

4. Run the app and browse to the home page. The markup in the conditional div
won't be rendered. Append the query string ?approved=true to the URL (for
example, http://localhost:1235/Home/Index?approved=true ). approved is set to
true and the conditional markup will be displayed.

7 Note

Use the nameof operator to specify the attribute to target rather than specifying a
string as you did with the bold tag helper:

C#

[HtmlTargetElement(Attributes = nameof(Condition))]
// [HtmlTargetElement(Attributes = "condition")]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
if (!Condition)
{
output.SuppressOutput();
}
}
}

The nameof operator will protect the code should it ever be refactored (we might
want to change the name to RedCondition ).

Avoid Tag Helper conflicts


In this section, you write a pair of auto-linking tag helpers. The first will replace markup
containing a URL starting with HTTP to an HTML anchor tag containing the same URL
(and thus yielding a link to the URL). The second will do the same for a URL starting with
WWW.

Because these two helpers are closely related and you may refactor them in the future,
we'll keep them in the same file.

1. Add the following AutoLinkerHttpTagHelper class to the TagHelpers folder.

C#

[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context,
TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor
tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http
link version}
}
}

7 Note

The AutoLinkerHttpTagHelper class targets p elements and uses Regex to


create the anchor.

2. Add the following markup to the end of the Views/Home/Contact.cshtml file:

CSHTML

@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>

<p>Visit us at http://docs.asp.net or at www.microsoft.com</p>

3. Run the app and verify that the tag helper renders the anchor correctly.

4. Update the AutoLinker class to include the AutoLinkerWwwTagHelper which will


convert www text to an anchor tag that also contains the original www text. The
updated code is highlighted below:

C#

[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext
context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their
anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http
link version}
}
}

[HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext
context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their
anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(www\.)(\S+)\b",
"<a target=\"_blank\" href=\"http://$0\">$0</a>"));
// www version
}
}
}
5. Run the app. Notice the www text is rendered as a link but the HTTP text isn't. If
you put a break point in both classes, you can see that the HTTP tag helper class
runs first. The problem is that the tag helper output is cached, and when the WWW
tag helper is run, it overwrites the cached output from the HTTP tag helper. Later
in the tutorial we'll see how to control the order that tag helpers run in. We'll fix
the code with the following:

C#

public class AutoLinkerHttpTagHelper : TagHelper


{
public override async Task ProcessAsync(TagHelperContext context,
TagHelperOutput output)
{
var childContent = output.Content.IsModified ?
output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();

// Find Urls in the content and replace them with their


anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http
link version}
}
}

[HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context,
TagHelperOutput output)
{
var childContent = output.Content.IsModified ?
output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();

// Find Urls in the content and replace them with their


anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(www\.)(\S+)\b",
"<a target=\"_blank\" href=\"http://$0\">$0</a>")); //
www version
}
}

7 Note
In the first edition of the auto-linking tag helpers, you got the content of the
target with the following code:

C#

var childContent = await output.GetChildContentAsync();

That is, you call GetChildContentAsync using the TagHelperOutput passed into
the ProcessAsync method. As mentioned previously, because the output is
cached, the last tag helper to run wins. You fixed that problem with the
following code:

C#

var childContent = output.Content.IsModified ?


output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();

The code above checks to see if the content has been modified, and if it has, it
gets the content from the output buffer.

6. Run the app and verify that the two links work as expected. While it might appear
our auto linker tag helper is correct and complete, it has a subtle problem. If the
WWW tag helper runs first, the www links won't be correct. Update the code by
adding the Order overload to control the order that the tag runs in. The Order
property determines the execution order relative to other tag helpers targeting the
same element. The default order value is zero and instances with lower values are
executed first.

C#

public class AutoLinkerHttpTagHelper : TagHelper


{
// This filter must run before the AutoLinkerWwwTagHelper as it
searches and replaces http and
// the AutoLinkerWwwTagHelper adds http to the markup.
public override int Order
{
get { return int.MinValue; }
}

The preceding code guarantees that the HTTP tag helper runs before the WWW
tag helper. Change Order to MaxValue and verify that the markup generated for
the WWW tag is incorrect.
Inspect and retrieve child content
The tag helpers provide several properties to retrieve content.

The result of GetChildContentAsync can be appended to output.Content .


You can inspect the result of GetChildContentAsync with GetContent .
If you modify output.Content , the TagHelper body won't be executed or rendered
unless you call GetChildContentAsync as in our auto-linker sample:

C#

public class AutoLinkerHttpTagHelper : TagHelper


{
public override async Task ProcessAsync(TagHelperContext context,
TagHelperOutput output)
{
var childContent = output.Content.IsModified ?
output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();

// Find Urls in the content and replace them with their anchor tag
equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link
version}
}
}

Multiple calls to GetChildContentAsync returns the same value and doesn't re-
execute the TagHelper body unless you pass in a false parameter indicating not to
use the cached result.

Load minified partial view TagHelper


In production environments, performance can be improved by loading minified partial
views. To take advantage of minified partial view in production:

Create/set up a pre-build process that minifies partial views.


Use the following code to load minified partial views in non-development
environments.

C#
public class MinifiedVersionPartialTagHelper : PartialTagHelper
{
public MinifiedVersionPartialTagHelper(ICompositeViewEngine
viewEngine,
IViewBufferScope viewBufferScope)
: base(viewEngine, viewBufferScope)
{

public override Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
// Append ".min" to load the minified partial view.
if (!IsDevelopment())
{
Name += ".min";
}

return base.ProcessAsync(context, output);


}

private bool IsDevelopment()


{
return
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
==
EnvironmentName.Development;
}
}
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Tag Helper Components in ASP.NET
Core
Article • 06/03/2022

By Scott Addie and Fiyaz Bin Hasan

A Tag Helper Component is a Tag Helper that allows you to conditionally modify or add
HTML elements from server-side code. This feature is available in ASP.NET Core 2.0 or
later.

ASP.NET Core includes two built-in Tag Helper Components: head and body . They're
located in the Microsoft.AspNetCore.Mvc.Razor.TagHelpers namespace and can be used
in both MVC and Razor Pages. Tag Helper Components don't require registration with
the app in _ViewImports.cshtml .

View or download sample code (how to download)

Use cases
Two common use cases of Tag Helper Components include:

1. Injecting a <link> into the <head>.


2. Injecting a <script> into the <body>.

The following sections describe these use cases.

Inject into HTML head element


Inside the HTML <head> element, CSS files are commonly imported with the HTML
<link> element. The following code injects a <link> element into the <head> element

using the head Tag Helper Component:

C#

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace RazorPagesSample.TagHelpers
{
public class AddressStyleTagHelperComponent : TagHelperComponent
{
private readonly string _style =
@"<link rel=""stylesheet"" href=""/css/address.css"" />";

public override int Order => 1;

public override Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
if (string.Equals(context.TagName, "head",
StringComparison.OrdinalIgnoreCase))
{
output.PostContent.AppendHtml(_style);
}

return Task.CompletedTask;
}
}
}

In the preceding code:

AddressStyleTagHelperComponent implements TagHelperComponent. The

abstraction:
Allows initialization of the class with a TagHelperContext.
Enables the use of Tag Helper Components to add or modify HTML elements.
The Order property defines the order in which the Components are rendered.
Order is necessary when there are multiple usages of Tag Helper Components in

an app.
ProcessAsync compares the execution context's TagName property value to head .
If the comparison evaluates to true, the content of the _style field is injected into
the HTML <head> element.

Inject into HTML body element


The body Tag Helper Component can inject a <script> element into the <body>
element. The following code demonstrates this technique:

C#

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace RazorPagesSample.TagHelpers
{
public class AddressScriptTagHelperComponent : TagHelperComponent
{
public override int Order => 2;

public override async Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
if (string.Equals(context.TagName, "body",
StringComparison.OrdinalIgnoreCase))
{
var script = await File.ReadAllTextAsync(
"TagHelpers/Templates/AddressToolTipScript.html");
output.PostContent.AppendHtml(script);
}
}
}
}

A separate HTML file is used to store the <script> element. The HTML file makes the
code cleaner and more maintainable. The preceding code reads the contents of
TagHelpers/Templates/AddressToolTipScript.html and appends it with the Tag Helper

output. The AddressToolTipScript.html file includes the following markup:

HTML

<script>
$("address[printable]").hover(function() {
$(this).attr({
"data-toggle": "tooltip",
"data-placement": "right",
"title": "Home of Microsoft!"
});
});
</script>

The preceding code binds a Bootstrap tooltip widget to any <address> element that
includes a printable attribute. The effect is visible when a mouse pointer hovers over
the element.

Register a Component
A Tag Helper Component must be added to the app's Tag Helper Components
collection. There are three ways to add to the collection:

Registration via services container


Registration via Razor file
Registration via Page Model or controller
Registration via services container
If the Tag Helper Component class isn't managed with ITagHelperComponentManager, it
must be registered with the dependency injection (DI) system. The following
Startup.ConfigureServices code registers the AddressStyleTagHelperComponent and

AddressScriptTagHelperComponent classes with a transient lifetime:

C#

public void ConfigureServices(IServiceCollection services)


{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.AddTransient<ITagHelperComponent,
AddressScriptTagHelperComponent>();
services.AddTransient<ITagHelperComponent,
AddressStyleTagHelperComponent>();
}

Registration via Razor file


If the Tag Helper Component isn't registered with DI, it can be registered from a Razor
Pages page or an MVC view. This technique is used for controlling the injected markup
and the component execution order from a Razor file.

ITagHelperComponentManager is used to add Tag Helper Components or remove them


from the app. The following code demonstrates this technique with
AddressTagHelperComponent :

CSHTML

@using RazorPagesSample.TagHelpers;
@using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
@inject ITagHelperComponentManager manager;

@{
string markup;

if (Model.IsWeekend)
{
markup = "<em class='text-warning'>Office closed today!</em>";
}
else
{
markup = "<em class='text-info'>Office open today!</em>";
}

manager.Components.Add(new AddressTagHelperComponent(markup, 1));


}

In the preceding code:

The @inject directive provides an instance of ITagHelperComponentManager . The


instance is assigned to a variable named manager for access downstream in the
Razor file.
An instance of AddressTagHelperComponent is added to the app's Tag Helper
Components collection.

AddressTagHelperComponent is modified to accommodate a constructor that accepts the

markup and order parameters:

C#

private readonly string _markup;

public override int Order { get; }

public AddressTagHelperComponent(string markup = "", int order = 1)


{
_markup = markup;
Order = order;
}

The provided markup parameter is used in ProcessAsync as follows:

C#

public override async Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
if (string.Equals(context.TagName, "address",
StringComparison.OrdinalIgnoreCase) &&
output.Attributes.ContainsName("printable"))
{
TagHelperContent childContent = await output.GetChildContentAsync();
string content = childContent.GetContent();
output.Content.SetHtmlContent(
$"<div>{content}<br>{_markup}</div>{_printableButton}");
}
}

Registration via Page Model or controller


If the Tag Helper Component isn't registered with DI, it can be registered from a Razor
Pages page model or an MVC controller. This technique is useful for separating C# logic
from Razor files.

Constructor injection is used to access an instance of ITagHelperComponentManager . The


Tag Helper Component is added to the instance's Tag Helper Components collection.
The following Razor Pages page model demonstrates this technique with
AddressTagHelperComponent :

C#

using System;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesSample.TagHelpers;

public class IndexModel : PageModel


{
private readonly ITagHelperComponentManager _tagHelperComponentManager;

public bool IsWeekend


{
get
{
var dayOfWeek = DateTime.Now.DayOfWeek;

return dayOfWeek == DayOfWeek.Saturday ||


dayOfWeek == DayOfWeek.Sunday;
}
}

public IndexModel(ITagHelperComponentManager tagHelperComponentManager)


{
_tagHelperComponentManager = tagHelperComponentManager;
}

public void OnGet()


{
string markup;

if (IsWeekend)
{
markup = "<em class='text-warning'>Office closed today!</em>";
}
else
{
markup = "<em class='text-info'>Office open today!</em>";
}

_tagHelperComponentManager.Components.Add(
new AddressTagHelperComponent(markup, 1));
}
}

In the preceding code:

Constructor injection is used to access an instance of ITagHelperComponentManager .


An instance of AddressTagHelperComponent is added to the app's Tag Helper
Components collection.

Create a Component
To create a custom Tag Helper Component:

Create a public class deriving from TagHelperComponentTagHelper.


Apply an [HtmlTargetElement] attribute to the class. Specify the name of the target
HTML element.
Optional: Apply an [EditorBrowsable(EditorBrowsableState.Never)] attribute to the
class to suppress the type's display in IntelliSense.

The following code creates a custom Tag Helper Component that targets the <address>
HTML element:

C#

using System.ComponentModel;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;

namespace RazorPagesSample.TagHelpers
{
[HtmlTargetElement("address")]
[EditorBrowsable(EditorBrowsableState.Never)]
public class AddressTagHelperComponentTagHelper :
TagHelperComponentTagHelper
{
public AddressTagHelperComponentTagHelper(
ITagHelperComponentManager componentManager,
ILoggerFactory loggerFactory) : base(componentManager,
loggerFactory)
{
}
}
}

Use the custom address Tag Helper Component to inject HTML markup as follows:

C#

public class AddressTagHelperComponent : TagHelperComponent


{
private readonly string _printableButton =
"<button type='button' class='btn btn-info' onclick=\"window.open("
+
"'https://binged.it/2AXRRYw')\">" +
"<span class='glyphicon glyphicon-road' aria-hidden='true'></span>"
+
"</button>";

public override int Order => 3;

public override async Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
if (string.Equals(context.TagName, "address",
StringComparison.OrdinalIgnoreCase) &&
output.Attributes.ContainsName("printable"))
{
var content = await output.GetChildContentAsync();
output.Content.SetHtmlContent(
$"<div>{content.GetContent()}</div>{_printableButton}");
}
}
}

The preceding ProcessAsync method injects the HTML provided to SetHtmlContent into
the matching <address> element. The injection occurs when:

The execution context's TagName property value equals address .


The corresponding <address> element has a printable attribute.

For example, the if statement evaluates to true when processing the following
<address> element:

CSHTML

<address printable>
One Microsoft Way<br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

Additional resources
Dependency injection in ASP.NET Core
Dependency injection into views in ASP.NET Core
ASP.NET Core built-in Tag Helpers
Anchor Tag Helper in ASP.NET Core
Article • 06/03/2022

By Peter Kellner and Scott Addie

The Anchor Tag Helper enhances the standard HTML anchor ( <a ... ></a> ) tag by
adding new attributes. By convention, the attribute names are prefixed with asp- . The
rendered anchor element's href attribute value is determined by the values of the asp-
attributes.

For an overview of Tag Helpers, see Tag Helpers in ASP.NET Core.

View or download sample code (how to download)

SpeakerController is used in samples throughout this document:

C#

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;

public class SpeakerController : Controller


{
private List<Speaker> Speakers =
new List<Speaker>
{
new Speaker {SpeakerId = 10},
new Speaker {SpeakerId = 11},
new Speaker {SpeakerId = 12}
};

[Route("Speaker/{id:int}")]
public IActionResult Detail(int id) =>
View(Speakers.FirstOrDefault(a => a.SpeakerId == id));

[Route("/Speaker/Evaluations",
Name = "speakerevals")]
public IActionResult Evaluations() => View();

[Route("/Speaker/EvaluationsCurrent",
Name = "speakerevalscurrent")]
public IActionResult Evaluations(
int speakerId,
bool currentYear) => View();

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


}

public class Speaker


{
public int SpeakerId { get; set; }
}

Anchor Tag Helper attributes

asp-controller
The asp-controller attribute assigns the controller used for generating the URL. The
following markup lists all speakers:

CSHTML

<a asp-controller="Speaker"
asp-action="Index">All Speakers</a>

The generated HTML:

HTML

<a href="/Speaker">All Speakers</a>

If the asp-controller attribute is specified and asp-action isn't, the default asp-action
value is the controller action associated with the currently executing view. If asp-action
is omitted from the preceding markup, and the Anchor Tag Helper is used in
HomeController's Index view (/Home), the generated HTML is:

HTML

<a href="/Home">All Speakers</a>

asp-action
The asp-action attribute value represents the controller action name included in the
generated href attribute. The following markup sets the generated href attribute value
to the speaker evaluations page:

CSHTML

<a asp-controller="Speaker"
asp-action="Evaluations">Speaker Evaluations</a>
The generated HTML:

HTML

<a href="/Speaker/Evaluations">Speaker Evaluations</a>

If no asp-controller attribute is specified, the default controller calling the view


executing the current view is used.

If the asp-action attribute value is Index , then no action is appended to the URL,
leading to the invocation of the default Index action. The action specified (or defaulted),
must exist in the controller referenced in asp-controller .

asp-route-{value}
The asp-route-{value} attribute enables a wildcard route prefix. Any value occupying the
{value} placeholder is interpreted as a potential route parameter. If a default route isn't

found, this route prefix is appended to the generated href attribute as a request
parameter and value. Otherwise, it's substituted in the route template.

Consider the following controller action:

C#

private List<Speaker> Speakers =


new List<Speaker>
{
new Speaker {SpeakerId = 10},
new Speaker {SpeakerId = 11},
new Speaker {SpeakerId = 12}
};

[Route("Speaker/{id:int}")]
public IActionResult Detail(int id) =>
View(Speakers.FirstOrDefault(a => a.SpeakerId == id));

With a default route template defined in Startup.Configure:

C#

app.UseMvc(routes =>
{
// need route and attribute on controller: [Area("Blogs")]
routes.MapRoute(name: "mvcAreaRoute",
template: "
{area:exists}/{controller=Home}/{action=Index}");
// default route for non-areas
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

The MVC view uses the model, provided by the action, as follows:

CSHTML

@model Speaker
<!DOCTYPE html>
<html>
<body>
<a asp-controller="Speaker"
asp-action="Detail"
asp-route-id="@Model.SpeakerId">SpeakerId: @Model.SpeakerId</a>
</body>
</html>

The default route's {id?} placeholder was matched. The generated HTML:

HTML

<a href="/Speaker/Detail/12">SpeakerId: 12</a>

Assume the route prefix isn't part of the matching routing template, as with the
following MVC view:

CSHTML

@model Speaker
<!DOCTYPE html>
<html>
<body>
<a asp-controller="Speaker"
asp-action="Detail"
asp-route-speakerid="@Model.SpeakerId">SpeakerId:
@Model.SpeakerId</a>
<body>
</html>

The following HTML is generated because speakerid wasn't found in the matching
route:

HTML
<a href="/Speaker/Detail?speakerid=12">SpeakerId: 12</a>

If either asp-controller or asp-action aren't specified, then the same default


processing is followed as is in the asp-route attribute.

asp-route
The asp-route attribute is used for creating a URL linking directly to a named route.
Using routing attributes, a route can be named as shown in the SpeakerController and
used in its Evaluations action:

C#

[Route("/Speaker/Evaluations",
Name = "speakerevals")]

In the following markup, the asp-route attribute references the named route:

CSHTML

<a asp-route="speakerevals">Speaker Evaluations</a>

The Anchor Tag Helper generates a route directly to that controller action using the URL
/Speaker/Evaluations. The generated HTML:

HTML

<a href="/Speaker/Evaluations">Speaker Evaluations</a>

If asp-controller or asp-action is specified in addition to asp-route , the route


generated may not be what you expect. To avoid a route conflict, asp-route shouldn't
be used with the asp-controller and asp-action attributes.

asp-all-route-data
The asp-all-route-data attribute supports the creation of a dictionary of key-value pairs.
The key is the parameter name, and the value is the parameter value.

In the following example, a dictionary is initialized and passed to a Razor view.


Alternatively, the data could be passed in with your model.
CSHTML

@{
var parms = new Dictionary<string, string>
{
{ "speakerId", "11" },
{ "currentYear", "true" }
};
}

<a asp-route="speakerevalscurrent"
asp-all-route-data="parms">Speaker Evaluations</a>

The preceding code generates the following HTML:

HTML

<a href="/Speaker/EvaluationsCurrent?speakerId=11&currentYear=true">Speaker
Evaluations</a>

The asp-all-route-data dictionary is flattened to produce a querystring meeting the


requirements of the overloaded Evaluations action:

C#

public IActionResult Evaluations() => View();

[Route("/Speaker/EvaluationsCurrent",
Name = "speakerevalscurrent")]
public IActionResult Evaluations(

If any keys in the dictionary match route parameters, those values are substituted in the
route as appropriate. The other non-matching values are generated as request
parameters.

asp-fragment
The asp-fragment attribute defines a URL fragment to append to the URL. The Anchor
Tag Helper adds the hash character (#). Consider the following markup:

CSHTML

<a asp-controller="Speaker"
asp-action="Evaluations"
asp-fragment="SpeakerEvaluations">Speaker Evaluations</a>
The generated HTML:

HTML

<a href="/Speaker/Evaluations#SpeakerEvaluations">Speaker Evaluations</a>

Hash tags are useful when building client-side apps. They can be used for easy marking
and searching in JavaScript, for example.

asp-area
The asp-area attribute sets the area name used to set the appropriate route. The
following examples depict how the asp-area attribute causes a remapping of routes.

Usage in Razor Pages


Razor Pages areas are supported in ASP.NET Core 2.1 or later.

Consider the following directory hierarchy:

{Project name}
wwwroot
Areas
Sessions
Pages
_ViewStart.cshtml
Index.cshtml

Index.cshtml.cs
Pages

The markup to reference the Sessions area Index Razor Page is:

CSHTML

<a asp-area="Sessions"
asp-page="/Index">View Sessions</a>

The generated HTML:

HTML

<a href="/Sessions">View Sessions</a>


 Tip

To support areas in a Razor Pages app, do one of the following in


Startup.ConfigureServices :

Set the compatibility version to 2.1 or later.

Set the RazorPagesOptions.AllowAreas property to true :

C#

services.AddMvc()
.AddRazorPagesOptions(options => options.AllowAreas =
true);

Usage in MVC

Consider the following directory hierarchy:

{Project name}
wwwroot
Areas
Blogs
Controllers
HomeController.cs

Views
Home
AboutBlog.cshtml

Index.cshtml
_ViewStart.cshtml
Controllers

Setting asp-area to "Blogs" prefixes the directory Areas/Blogs to the routes of the
associated controllers and views for this anchor tag. The markup to reference the
AboutBlog view is:

CSHTML

<a asp-area="Blogs"
asp-controller="Home"
asp-action="AboutBlog">About Blog</a>
The generated HTML:

HTML

<a href="/Blogs/Home/AboutBlog">About Blog</a>

 Tip

To support areas in an MVC app, the route template must include a reference to the
area, if it exists. That template is represented by the second parameter of the
routes.MapRoute method call in Startup.Configure:

C#

app.UseMvc(routes =>
{
// need route and attribute on controller: [Area("Blogs")]
routes.MapRoute(name: "mvcAreaRoute",
template: "
{area:exists}/{controller=Home}/{action=Index}");

// default route for non-areas


routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

asp-protocol
The asp-protocol attribute is for specifying a protocol (such as https ) in your URL. For
example:

CSHTML

<a asp-protocol="https"
asp-controller="Home"
asp-action="About">About</a>

The generated HTML:

HTML

<a href="https://localhost/Home/About">About</a>
The host name in the example is localhost. The Anchor Tag Helper uses the website's
public domain when generating the URL.

asp-host
The asp-host attribute is for specifying a host name in your URL. For example:

CSHTML

<a asp-protocol="https"
asp-host="microsoft.com"
asp-controller="Home"
asp-action="About">About</a>

The generated HTML:

HTML

<a href="https://microsoft.com/Home/About">About</a>

asp-page
The asp-page attribute is used with Razor Pages. Use it to set an anchor tag's href
attribute value to a specific page. Prefixing the page name with / creates a URL for a
matching page from the root of the app:

With the sample code, the following markup creates a link to the attendee Razor Page:

CSHTML

<a asp-page="/Attendee">All Attendees</a>

The generated HTML:

HTML

<a href="/Attendee">All Attendees</a>

The asp-page attribute is mutually exclusive with the asp-route , asp-controller , and
asp-action attributes. However, asp-page can be used with asp-route-{value} to
control routing, as the following markup demonstrates:

CSHTML
<a asp-page="/Attendee"
asp-route-attendeeid="10">View Attendee</a>

The generated HTML:

HTML

<a href="/Attendee?attendeeid=10">View Attendee</a>

If the referenced page doesn't exist, a link to the current page is generated using an
ambient value from the request. No warning is indicated, except at the debug log level.

asp-page-handler
The asp-page-handler attribute is used with Razor Pages. It's intended for linking to
specific page handlers.

Consider the following page handler:

C#

public void OnGetProfile(int attendeeId)


{
ViewData["AttendeeId"] = attendeeId;

// code omitted for brevity


}

The page model's associated markup links to the OnGetProfile page handler. Note the
On<Verb> prefix of the page handler method name is omitted in the asp-page-handler
attribute value. When the method is asynchronous, the Async suffix is omitted, too.

CSHTML

<a asp-page="/Attendee"
asp-page-handler="Profile"
asp-route-attendeeid="12">Attendee Profile</a>

The generated HTML:

HTML

<a href="/Attendee?attendeeid=12&handler=Profile">Attendee Profile</a>


Additional resources
Areas in ASP.NET Core
Introduction to Razor Pages in ASP.NET Core
Compatibility version for ASP.NET Core MVC
Cache Tag Helper in ASP.NET Core MVC
Article • 06/03/2022

By Peter Kellner

The Cache Tag Helper provides the ability to improve the performance of your ASP.NET
Core app by caching its content to the internal ASP.NET Core cache provider.

For an overview of Tag Helpers, see Tag Helpers in ASP.NET Core.

The following Razor markup caches the current date:

CSHTML

<cache>@DateTime.Now</cache>

The first request to the page that contains the Tag Helper displays the current date.
Additional requests show the cached value until the cache expires (default 20 minutes)
or until the cached date is evicted from the cache.

Cache Tag Helper Attributes

enabled

Attribute Type Examples Default

Boolean true , false true

enabled determines if the content enclosed by the Cache Tag Helper is cached. The
default is true . If set to false , the rendered output is not cached.

Example:

CSHTML

<cache enabled="true">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

expires-on
Attribute Type Example

DateTimeOffset @new DateTime(2025,1,29,17,02,0)

expires-on sets an absolute expiration date for the cached item.

The following example caches the contents of the Cache Tag Helper until 5:02 PM on
January 29, 2025:

CSHTML

<cache expires-on="@new DateTime(2025,1,29,17,02,0)">


Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

expires-after

Attribute Type Example Default

TimeSpan @TimeSpan.FromSeconds(120) 20 minutes

expires-after sets the length of time from the first request time to cache the contents.

Example:

CSHTML

<cache expires-after="@TimeSpan.FromSeconds(120)">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

The Razor View Engine sets the default expires-after value to twenty minutes.

expires-sliding

Attribute Type Example

TimeSpan @TimeSpan.FromSeconds(60)

Sets the time that a cache entry should be evicted if its value hasn't been accessed.

Example:

CSHTML
<cache expires-sliding="@TimeSpan.FromSeconds(60)">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-header

Attribute Type Examples

String User-Agent , User-Agent,content-encoding

vary-by-header accepts a comma-delimited list of header values that trigger a cache


refresh when they change.

The following example monitors the header value User-Agent . The example caches the
content for every different User-Agent presented to the web server:

CSHTML

<cache vary-by-header="User-Agent">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-query

Attribute Type Examples

String Make , Make,Model

vary-by-query accepts a comma-delimited list of Keys in a query string (Query) that


trigger a cache refresh when the value of any listed key changes.

The following example monitors the values of Make and Model . The example caches the
content for every different Make and Model presented to the web server:

CSHTML

<cache vary-by-query="Make,Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-route
Attribute Type Examples

String Make , Make,Model

vary-by-route accepts a comma-delimited list of route parameter names that trigger a

cache refresh when the route data parameter value changes.

Example:

Startup.cs :

C#

routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{Make?}/{Model?}");

Index.cshtml :

CSHTML

<cache vary-by-route="Make,Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-cookie

Attribute Examples
Type

String .AspNetCore.Identity.Application ,
.AspNetCore.Identity.Application,HairColor

vary-by-cookie accepts a comma-delimited list of cookie names that trigger a cache


refresh when the cookie values change.

The following example monitors the cookie associated with ASP.NET Core Identity.
When a user is authenticated, a change in the Identity cookie triggers a cache refresh:

CSHTML

<cache vary-by-cookie=".AspNetCore.Identity.Application">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
vary-by-user

Attribute Type Examples Default

Boolean true , false true

vary-by-user specifies whether or not the cache resets when the signed-in user (or
Context Principal) changes. The current user is also known as the Request Context
Principal and can be viewed in a Razor view by referencing @User.Identity.Name .

The following example monitors the current logged in user to trigger a cache refresh:

CSHTML

<cache vary-by-user="true">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

Using this attribute maintains the contents in cache through a sign-in and sign-out
cycle. When the value is set to true , an authentication cycle invalidates the cache for
the authenticated user. The cache is invalidated because a new unique cookie value is
generated when a user is authenticated. Cache is maintained for the anonymous state
when no cookie is present or the cookie has expired. If the user is not authenticated, the
cache is maintained.

vary-by

Attribute Type Example

String @Model

vary-by allows for customization of what data is cached. When the object referenced by
the attribute's string value changes, the content of the Cache Tag Helper is updated.
Often, a string-concatenation of model values are assigned to this attribute. Effectively,
this results in a scenario where an update to any of the concatenated values invalidates
the cache.

The following example assumes the controller method rendering the view sums the
integer value of the two route parameters, myParam1 and myParam2 , and returns the sum
as the single model property. When this sum changes, the content of the Cache Tag
Helper is rendered and cached again.

Action:
C#

public IActionResult Index(string myParam1, string myParam2, string


myParam3)
{
int num1;
int num2;
int.TryParse(myParam1, out num1);
int.TryParse(myParam2, out num2);
return View(viewName, num1 + num2);
}

Index.cshtml :

CSHTML

<cache vary-by="@Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

priority

Attribute Type Examples Default

CacheItemPriority High , Low , NeverRemove , Normal Normal

priority provides cache eviction guidance to the built-in cache provider. The web

server evicts Low cache entries first when it's under memory pressure.

Example:

CSHTML

<cache priority="High">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

The priority attribute doesn't guarantee a specific level of cache retention.


CacheItemPriority is only a suggestion. Setting this attribute to NeverRemove doesn't
guarantee that cached items are always retained. See the topics in the Additional
Resources section for more information.

The Cache Tag Helper is dependent on the memory cache service. The Cache Tag Helper
adds the service if it hasn't been added.
Additional resources
Cache in-memory in ASP.NET Core
Introduction to Identity on ASP.NET Core
Component Tag Helper in ASP.NET Core
Article • 06/03/2022

Prerequisites
Follow the guidance in the Configuration section for either:

Blazor Server: Integrate routable and non-routable Razor components into Razor
Pages and MVC apps.
Blazor WebAssembly: Integrate Razor components from a hosted Blazor
WebAssembly solution into Razor Pages and MVC apps.

Component Tag Helper


To render a component from a page or view, use the Component Tag Helper
( <component> tag).

7 Note

Integrating Razor components into Razor Pages and MVC apps in a hosted Blazor
WebAssembly app is supported in ASP.NET Core in .NET 5.0 or later.

RenderMode configures whether the component:

Is prerendered into the page.


Is rendered as static HTML on the page or if it includes the necessary information
to bootstrap a Blazor app from the user agent.

Blazor WebAssembly app render modes are shown in the following table.

Render Mode Description

WebAssembly Renders a marker for a Blazor WebAssembly app for use to include an
interactive component when loaded in the browser. The component
isn't prerendered. This option makes it easier to render different Blazor
WebAssembly components on different pages.

WebAssemblyPrerendered Prerenders the component into static HTML and includes a marker for
a Blazor WebAssembly app for later use to make the component
interactive when loaded in the browser.
Blazor Server app render modes are shown in the following table.

Render Mode Description

ServerPrerendered Renders the component into static HTML and includes a marker for a Blazor
Server app. When the user-agent starts, this marker is used to bootstrap a
Blazor app.

Server Renders a marker for a Blazor Server app. Output from the component isn't
included. When the user-agent starts, this marker is used to bootstrap a
Blazor app.

Static Renders the component into static HTML.

Additional characteristics include:

Multiple Component Tag Helpers rendering multiple Razor components is allowed.


Components can't be dynamically rendered after the app has started.
While pages and views can use components, the converse isn't true. Components
can't use view- and page-specific features, such as partial views and sections. To
use logic from a partial view in a component, factor out the partial view logic into a
component.
Rendering server components from a static HTML page isn't supported.

The following Component Tag Helper renders the Counter component in a page or view
in a Blazor Server app with ServerPrerendered :

CSHTML

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}.Pages

...

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

The preceding example assumes that the Counter component is in the app's Pages
folder. The placeholder {APP ASSEMBLY} is the app's assembly name (for example, @using
BlazorSample.Pages or @using BlazorSample.Client.Pages in a hosted Blazor solution).

The Component Tag Helper can also pass parameters to components. Consider the
following ColorfulCheckbox component that sets the checkbox label's color and size:

razor
<label style="font-size:@(Size)px;color:@Color">
<input @bind="Value"
id="survey"
name="blazor"
type="checkbox" />
Enjoying Blazor?
</label>

@code {
[Parameter]
public bool Value { get; set; }

[Parameter]
public int Size { get; set; } = 8;

[Parameter]
public string Color { get; set; }

protected override void OnInitialized()


{
Size += 10;
}
}

The Size ( int ) and Color ( string ) component parameters can be set by the
Component Tag Helper:

CSHTML

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}.Shared

...

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


param-Size="14" param-Color="@("blue")" />

The preceding example assumes that the ColorfulCheckbox component is in the app's
Shared folder. The placeholder {APP ASSEMBLY} is the app's assembly name (for example,
@using BlazorSample.Shared ).

The following HTML is rendered in the page or view:

HTML

<label style="font-size:24px;color:blue">
<input id="survey" name="blazor" type="checkbox">
Enjoying Blazor?
</label>
Passing a quoted string requires an explicit Razor expression, as shown for param-Color
in the preceding example. The Razor parsing behavior for a string type value doesn't
apply to a param-* attribute because the attribute is an object type.

All types of parameters are supported, except:

Generic parameters.
Non-serializable parameters.
Inheritance in collection parameters.
Parameters whose type is defined outside of the Blazor WebAssembly app or
within a lazily-loaded assembly.
For receiving a RenderFragment delegate for child content (for example, param-
ChildContent="..." ). For this scenario, we recommend creating a Razor

component ( .razor ) that references the component you want to render with the
child content you want to pass and then invoke the Razor component from the
page or view with the Component Tag Helper.

The parameter type must be JSON serializable, which typically means that the type must
have a default constructor and settable properties. For example, you can specify a value
for Size and Color in the preceding example because the types of Size and Color are
primitive types ( int and string ), which are supported by the JSON serializer.

In the following example, a class object is passed to the component:

MyClass.cs :

C#

public class MyClass


{
public MyClass()
{
}

public int MyInt { get; set; } = 999;


public string MyString { get; set; } = "Initial value";
}

The class must have a public parameterless constructor.

Shared/MyComponent.razor :

razor
<h2>MyComponent</h2>

<p>Int: @MyObject.MyInt</p>
<p>String: @MyObject.MyString</p>

@code
{
[Parameter]
public MyClass MyObject { get; set; }
}

Pages/MyPage.cshtml :

CSHTML

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}
@using {APP ASSEMBLY}.Shared

...

@{
var myObject = new MyClass();
myObject.MyInt = 7;
myObject.MyString = "Set by MyPage";
}

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


param-MyObject="@myObject" />

The preceding example assumes that the MyComponent component is in the app's Shared
folder. The placeholder {APP ASSEMBLY} is the app's assembly name (for example, @using
BlazorSample and @using BlazorSample.Shared ). MyClass is in the app's namespace.

Additional resources
ComponentTagHelper
Tag Helpers in ASP.NET Core
ASP.NET Core Razor components
Distributed Cache Tag Helper in ASP.NET
Core
Article • 06/03/2022

By Peter Kellner

The Distributed Cache Tag Helper provides the ability to dramatically improve the
performance of your ASP.NET Core app by caching its content to a distributed cache
source.

For an overview of Tag Helpers, see Tag Helpers in ASP.NET Core.

The Distributed Cache Tag Helper inherits from the same base class as the Cache Tag
Helper. All of the Cache Tag Helper attributes are available to the Distributed Tag Helper.

The Distributed Cache Tag Helper uses constructor injection. The IDistributedCache
interface is passed into the Distributed Cache Tag Helper's constructor. If no concrete
implementation of IDistributedCache is created in Startup.ConfigureServices
( Startup.cs ), the Distributed Cache Tag Helper uses the same in-memory provider for
storing cached data as the Cache Tag Helper.

Distributed Cache Tag Helper Attributes

Attributes shared with the Cache Tag Helper


enabled

expires-on
expires-after

expires-sliding

vary-by-header
vary-by-query

vary-by-route
vary-by-cookie

vary-by-user

vary-by
priority

The Distributed Cache Tag Helper inherits from the same class as Cache Tag Helper. For
descriptions of these attributes, see the Cache Tag Helper.
name

Attribute Type Example

String my-distributed-cache-unique-key-101

name is required. The name attribute is used as a key for each stored cache instance.
Unlike the Cache Tag Helper that assigns a cache key to each instance based on the
Razor page name and location in the Razor page, the Distributed Cache Tag Helper only
bases its key on the attribute name .

Example:

CSHTML

<distributed-cache name="my-distributed-cache-unique-key-101">
Time Inside Cache Tag Helper: @DateTime.Now
</distributed-cache>

Distributed Cache Tag Helper IDistributedCache


implementations
There are two implementations of IDistributedCache built in to ASP.NET Core. One is
based on SQL Server, and the other is based on Redis. Third-party implementations are
also available, such as NCache . Details of these implementations can be found at
Distributed caching in ASP.NET Core. Both implementations involve setting an instance
of IDistributedCache in Startup .

There are no tag attributes specifically associated with using any specific
implementation of IDistributedCache .

Additional resources
Cache Tag Helper in ASP.NET Core MVC
Dependency injection in ASP.NET Core
Distributed caching in ASP.NET Core
Cache in-memory in ASP.NET Core
Introduction to Identity on ASP.NET Core
Environment Tag Helper in ASP.NET
Core
Article • 06/03/2022

By Peter Kellner and Hisham Bin Ateya

The Environment Tag Helper conditionally renders its enclosed content based on the
current hosting environment. The Environment Tag Helper's single attribute, names , is a
comma-separated list of environment names. If any of the provided environment names
match the current environment, the enclosed content is rendered.

For an overview of Tag Helpers, see Tag Helpers in ASP.NET Core.

Environment Tag Helper Attributes

names
names accepts a single hosting environment name or a comma-separated list of hosting
environment names that trigger the rendering of the enclosed content.

Environment values are compared to the current value returned by


IWebHostEnvironment.EnvironmentName. The comparison ignores case.

The following example uses an Environment Tag Helper. The content is rendered if the
hosting environment is Staging or Production:

CSHTML

<environment names="Staging,Production">
<strong>IWebHostEnvironment.EnvironmentName is Staging or
Production</strong>
</environment>

include and exclude attributes


include & exclude attributes control rendering the enclosed content based on the
included or excluded hosting environment names.

include
The include property exhibits similar behavior to the names attribute. An environment
listed in the include attribute value must match the app's hosting environment
(IWebHostEnvironment.EnvironmentName) to render the content of the <environment>
tag.

CSHTML

<environment include="Staging,Production">
<strong>IWebHostEnvironment.EnvironmentName is Staging or
Production</strong>
</environment>

exclude
In contrast to the include attribute, the content of the <environment> tag is rendered
when the hosting environment doesn't match an environment listed in the exclude
attribute value.

CSHTML

<environment exclude="Development">
<strong>IWebHostEnvironment.EnvironmentName is not Development</strong>
</environment>

Additional resources
Use multiple environments in ASP.NET Core
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Image Tag Helper in ASP.NET Core
Article • 06/03/2022

By Peter Kellner

The Image Tag Helper enhances the <img> tag to provide cache-busting behavior for
static image files.

A cache-busting string is a unique value representing the hash of the static image file
appended to the asset's URL. The unique string prompts clients (and some proxies) to
reload the image from the host web server and not from the client's cache.

If the image source ( src ) is a static file on the host web server:

A unique cache-busting string is appended as a query parameter to the image


source.
If the file on the host web server changes, a unique request URL is generated that
includes the updated request parameter.

For an overview of Tag Helpers, see Tag Helpers in ASP.NET Core.

Image Tag Helper Attributes

src
To activate the Image Tag Helper, the src attribute is required on the <img> element.

The image source ( src ) must point to a physical static file on the server. If the src is a
remote URI, the cache-busting query string parameter isn't generated.

asp-append-version
When asp-append-version is specified with a true value along with a src attribute, the
Image Tag Helper is invoked.

The following example uses an Image Tag Helper:

CSHTML

<img src="~/images/asplogo.png" asp-append-version="true">


If the static file exists in the directory /wwwroot/images/, the generated HTML is similar
to the following (the hash will be different):

HTML

<img src="/images/asplogo.png?
v=Kl_dqr9NVtnMdsM2MUg4qthUnWZm5T1fCEimBPWDNgM">

The value assigned to the parameter v is the hash value of the asplogo.png file on disk.
If the web server is unable to obtain read access to the static file, no v parameter is
added to the src attribute in the rendered markup.

For a Tag Helper to generate a version for a static file outside wwwroot , see Serve files
from multiple locations

Hash caching behavior


The Image Tag Helper uses the cache provider on the local web server to store the
calculated Sha512 hash of a given file. If the file is requested multiple times, the hash
isn't recalculated. The cache is invalidated by a file watcher that's attached to the file
when the file's Sha512 hash is calculated. When the file changes on disk, a new hash is
calculated and cached.

Additional resources
Cache in-memory in ASP.NET Core
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Link Tag Helper in ASP.NET Core
Article • 06/27/2022

By Rick Anderson

The Link Tag Helper generates a link to a primary or fall back CSS file. Typically the
primary CSS file is on a Content Delivery Network (CDN).

A CDN:

Provides several performance advantages vs hosting the asset with the web app.
Should not be relied on as the only source for the asset. CDNs are not always
available, therefore a reliable fallback should be used. Typically the fallback is the
site hosting the web app.

The Link Tag Helper allows you to specify a CDN for the CSS file and a fallback when the
CDN is not available. The Link Tag Helper provides the performance advantage of a CDN
with the robustness of local hosting.

The following Razor markup shows the head element of a layout file created with the
ASP.NET Core web app template:

CSHTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebLinkTH</title>

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css"
/>
</environment>
<environment exclude="Development">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/css/bootstrap.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
asp-fallback-test-class="sr-only" asp-fallback-test-
property="position"
asp-fallback-test-value="absolute"
crossorigin="anonymous"
integrity="sha256-
eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" />
</environment>
<link rel="stylesheet" href="~/css/site.css" />
</head>

The following is rendered HTML from the preceding code (in a non-Development
environment):

HTML

<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home page - WebLinkTH</title>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/css/bootstrap.css"
crossorigin="anonymous" integrity="sha256-eS<snip>BE=" />
<meta name="x-stylesheet-fallback-test" content="" class="sr-only" />
<script>
!function (a, b, c, d) {
var e, f = document,
g = f.getElementsByTagName("SCRIPT"),
h = g[g.length - 1].previousElementSibling,
i = f.defaultView && f.defaultView.getComputedStyle ?
f.defaultView.getComputedStyle(h) : h.currentStyle;
if (i && i[a] !== b) for (e = 0; e < c.length; e++)
f.write('<link href="' + c[e] + '" ' + d + "/>")
}
("position", "absolute",
["\/lib\/bootstrap\/dist\/css\/bootstrap.css"],
"rel=\u0022stylesheet\u0022
crossorigin=\u0022anonymous\u0022 integrity=\abc<snip>BE=\u0022 ");
</script>

<link rel="stylesheet" href="/css/site.css" />


</head>

In the preceding code, the Link Tag Helper generated the <meta name="x-stylesheet-
fallback-test" content="" class="sr-only" /> element and the following JavaScript

which is used to verify the requested bootstrap.css file is available on the CDN. In this
case, the CSS file was available so the Tag Helper generated the <link /> element with
the CDN CSS file.

Commonly used Link Tag Helper attributes


See Link Tag Helper for all the Link Tag Helper attributes, properties, and methods.
href
Preferred address of the linked resource. The address is passed thought to the
generated HTML in all cases.

asp-fallback-href
The URL of a CSS stylesheet to fallback to in the case the primary URL fails.

asp-fallback-test-class
The class name defined in the stylesheet to use for the fallback test. For more
information, see FallbackTestClass.

asp-fallback-test-property
The CSS property name to use for the fallback test. For more information, see
FallbackTestProperty.

asp-fallback-test-value
The CSS property value to use for the fallback test. For more information, see
FallbackTestValue.

Additional resources
Tag Helpers in ASP.NET Core
Areas in ASP.NET Core
Introduction to Razor Pages in ASP.NET Core
Compatibility version for ASP.NET Core MVC
Partial Tag Helper in ASP.NET Core
Article • 06/03/2022

By Scott Addie

For an overview of Tag Helpers, see Tag Helpers in ASP.NET Core.

View or download sample code (how to download)

Overview
The Partial Tag Helper is used for rendering a partial view in Razor Pages and MVC apps.
Consider that it:

Requires ASP.NET Core 2.1 or later.


Is an alternative to HTML Helper syntax.
Renders the partial view asynchronously.

The HTML Helper options for rendering a partial view include:

@await Html.PartialAsync
@await Html.RenderPartialAsync
@Html.Partial
@Html.RenderPartial

The Product model is used in samples throughout this document:

C#

namespace TagHelpersBuiltIn.Models
{
public class Product
{
public int Number { get; set; }

public string Name { get; set; }

public string Description { get; set; }


}
}

An inventory of the Partial Tag Helper attributes follows.

name
The name attribute is required. It indicates the name or the path of the partial view to be
rendered. When a partial view name is provided, the view discovery process is initiated.
That process is bypassed when an explicit path is provided. For all acceptable name
values, see Partial view discovery.

The following markup uses an explicit path, indicating that _ProductPartial.cshtml is to


be loaded from the Shared folder. Using the for attribute, a model is passed to the
partial view for binding.

CSHTML

<partial name="Shared/_ProductPartial.cshtml" for="Product">

for
The for attribute assigns a ModelExpression to be evaluated against the current model.
A ModelExpression infers the @Model. syntax. For example, for="Product" can be used
instead of for="@Model.Product" . This default inference behavior is overridden by using
the @ symbol to define an inline expression.

The following markup loads _ProductPartial.cshtml :

CSHTML

<partial name="_ProductPartial" for="Product">

The partial view is bound to the associated page model's Product property:

C#

using Microsoft.AspNetCore.Mvc.RazorPages;
using TagHelpersBuiltIn.Models;

namespace TagHelpersBuiltIn.Pages
{
public class ProductModel : PageModel
{
public Product Product { get; set; }

public void OnGet()


{
Product = new Product
{
Number = 1,
Name = "Test product",
Description = "This is a test product"
};
}
}
}

model
The model attribute assigns a model instance to pass to the partial view. The model
attribute can't be used with the for attribute.

In the following markup, a new Product object is instantiated and passed to the model
attribute for binding:

CSHTML

<partial name="_ProductPartial"
model='new Product { Number = 1, Name = "Test product", Description
= "This is a test" }'>

view-data
The view-data attribute assigns a ViewDataDictionary to pass to the partial view. The
following markup makes the entire ViewData collection accessible to the partial view:

CSHTML

@{
ViewData["IsNumberReadOnly"] = true;
}

<partial name="_ProductViewDataPartial" for="Product" view-data="ViewData">

In the preceding code, the IsNumberReadOnly key value is set to true and added to the
ViewData collection. Consequently, ViewData["IsNumberReadOnly"] is made accessible
within the following partial view:

CSHTML

@model TagHelpersBuiltIn.Models.Product

<div class="form-group">
<label asp-for="Number"></label>
@if ((bool)ViewData["IsNumberReadOnly"])
{
<input asp-for="Number" type="number" class="form-control" readonly
/>
}
else
{
<input asp-for="Number" type="number" class="form-control" />
}
</div>
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" type="text" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Description"></label>
<textarea asp-for="Description" rows="4" cols="50" class="form-control">
</textarea>
</div>

In this example, the value of ViewData["IsNumberReadOnly"] determines whether the


Number field is displayed as read only.

Migrate from an HTML Helper


Consider the following asynchronous HTML Helper example. A collection of products is
iterated and displayed. Per the PartialAsync method's first parameter, the
_ProductPartial.cshtml partial view is loaded. An instance of the Product model is
passed to the partial view for binding.

CSHTML

@foreach (var product in Model.Products)


{
@await Html.PartialAsync("_ProductPartial", product)
}

The following Partial Tag Helper achieves the same asynchronous rendering behavior as
the PartialAsync HTML Helper. The model attribute is assigned a Product model
instance for binding to the partial view.

CSHTML

@foreach (var product in Model.Products)


{
<partial name="_ProductPartial" model="product" />
}
Additional resources
Partial views in ASP.NET Core
Views in ASP.NET Core MVC
Persist Component State Tag Helper in
ASP.NET Core
Article • 11/08/2022

Prerequisites
Follow the guidance in the Configuration section for either:

Blazor WebAssembly
Blazor Server

Persist state for prerendered components


To persist state for prerendered components, use the Persist Component State Tag
Helper (reference source ). Add the Tag Helper's tag, <persist-component-state /> ,
inside the closing </body> tag of the _Host page in an app that prerenders
components.

7 Note

Documentation links to .NET reference source usually load the repository's default
branch, which represents the current development for the next release of .NET. To
select a tag for a specific release, use the Switch branches or tags dropdown list.
For more information, see How to select a version tag of ASP.NET Core source
code (dotnet/AspNetCore.Docs #26205) .

In Blazor WebAssembly apps ( Pages/_Host.cshtml ):

CSHTML

<body>
<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />

...

<persist-component-state />
</body>

In Blazor Server apps ( Pages/_Host.cshtml ):


CSHTML

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

...

<persist-component-state />
</body>

Decide what state to persist using the PersistentComponentState service.


PersistentComponentState.RegisterOnPersisting registers a callback to persist the
component state before the app is paused. The state is retrieved when the application
resumes.

In the following example:

The {TYPE} placeholder represents the type of data to persist (for example,
WeatherForecast[] ).

The {TOKEN} placeholder is a state identifier string (for example, fetchdata ).

razor

@implements IDisposable
@inject PersistentComponentState ApplicationState

...

@code {
private {TYPE} data;
private PersistingComponentStateSubscription persistingSubscription;

protected override async Task OnInitializedAsync()


{
persistingSubscription =
ApplicationState.RegisterOnPersisting(PersistData);

if (!ApplicationState.TryTakeFromJson<{TYPE}>(
"{TOKEN}", out var restored))
{
data = await ...;
}
else
{
data = restored!;
}
}

private Task PersistData()


{
ApplicationState.PersistAsJson("{TOKEN}", data);

return Task.CompletedTask;
}

void IDisposable.Dispose()
{
persistingSubscription.Dispose();
}
}

For more information and a complete example, see Prerender and integrate ASP.NET
Core Razor components.

Prerendered state size and SignalR message


size limit
This section only applies to Blazor Server apps.

A large prerendered state size may exceed the SignalR circuit message size limit, which
results in the following:

The SignalR circuit fails to initialize with an error on the client: Circuit host not
initialized.
The reconnection dialog on the client appears when the circuit fails. Recovery isn't
possible.

To resolve the problem, use either of the following approaches:

Reduce the amount of data that you are putting into the prerendered state.
Increase the SignalR message size limit. WARNING: Increasing the limit may
increase the risk of Denial of service (DoS) attacks.

Additional resources
ComponentTagHelper
Tag Helpers in ASP.NET Core
ASP.NET Core Razor components
Script Tag Helper in ASP.NET Core
Article • 04/01/2023

By Rick Anderson

The Script Tag Helper generates a link to a primary or fall back script file. Typically the
primary script file is on a Content Delivery Network (CDN).

A CDN:

Provides several performance advantages vs hosting the asset with the web app.
Should not be relied on as the only source for the asset. CDNs are not always
available, therefore a reliable fallback should be used. Typically the fallback is the
site hosting the web app.

The Script Tag Helper allows you to specify a CDN for the script file and a fallback when
the CDN is not available. The Script Tag Helper provides the performance advantage of a
CDN with the robustness of local hosting.

The following Razor markup shows a script element with a fallback:

HTML

<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.js"
asp-fallback-src="~/lib/jquery/dist/jquery.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-
tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
</script>

Don't use the <script> element's defer attribute to defer loading the CDN script. The
Script Tag Helper renders JavaScript that immediately executes the asp-fallback-test
expression. The expression fails if loading the CDN script is deferred.

Commonly used Script Tag Helper attributes


See Script Tag Helper for all the Script Tag Helper attributes, properties, and methods.

src
Address of the external script to use.
asp-append-version
When asp-append-version is specified with a true value along with a src attribute, a
unique version is generated.

For a Tag Helper to generate a version for a static file outside wwwroot , see Serve files
from multiple locations

asp-fallback-src
The URL of a Script tag to fallback to in the case the primary one fails.

asp-fallback-src-exclude
A comma-separated list of globbed file patterns of JavaScript scripts to exclude from the
fallback list, in the case the primary one fails. The glob patterns are assessed relative to
the application's webroot setting. Must be used in conjunction with asp-fallback-src-
include .

asp-fallback-src-include
A comma-separated list of globbed file patterns of JavaScript scripts to fallback to in the
case the primary one fails. The glob patterns are assessed relative to the application's
webroot setting.

asp-fallback-test
The script method defined in the primary script to use for the fallback test. For more
information, see FallbackTestExpression.

asp-order
When a set of ITagHelper instances are executed, their Init(TagHelperContext)
methods are first invoked in the specified order; then their
ProcessAsync(TagHelperContext, TagHelperOutput) methods are invoked in the specified
order. Lower values are executed first.

asp-src-exclude
A comma-separated list of globbed file patterns of JavaScript scripts to exclude from
loading. The glob patterns are assessed relative to the application's webroot setting.
Must be used in conjunction with asp-src-include .

asp-src-include
A comma-separated list of globbed file patterns of JavaScript scripts to load. The glob
patterns are assessed relative to the application's webroot setting.

asp-suppress-fallback-integrity
Boolean value that determines if an integrity hash will be compared with the asp-
fallback-src value.

Additional resources
Tag Helpers in ASP.NET Core
Areas in ASP.NET Core
Introduction to Razor Pages in ASP.NET Core
Compatibility version for ASP.NET Core MVC
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023

By Rick Anderson , N. Taylor Mullen , Dave Paquette , and Jerrie Pelser

This document demonstrates working with Forms and the HTML elements commonly
used on a Form. The HTML Form element provides the primary mechanism web apps
use to post back data to the server. Most of this document describes Tag Helpers and
how they can help you productively create robust HTML forms. We recommend you
read Introduction to Tag Helpers before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper,
but it's important to recognize that Tag Helpers don't replace HTML Helpers and there's
not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's
mentioned.

The Form Tag Helper


The Form Tag Helper:

Generates the HTML <FORM> action attribute value for a MVC controller
action or named route

Generates a hidden Request Verification Token to prevent cross-site request


forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP
Post action method)

Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is


added to the route values. The routeValues parameters to Html.BeginForm and
Html.BeginRouteForm provide similar functionality.

Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm

Sample:

CSHTML

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

The Form Tag Helper above generates the following HTML:


HTML

<form method="post" action="/Demo/Register">


<!-- Input and Submit elements -->
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The MVC runtime generates the action attribute value from the Form Tag Helper
attributes asp-controller and asp-action . The Form Tag Helper also generates a
hidden Request Verification Token to prevent cross-site request forgery (when used with
the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper
provides this service for you.

Using a named route


The asp-route Tag Helper attribute can also generate markup for the HTML action
attribute. An app with a route named register could use the following markup for the
registration page:

CSHTML

<form asp-route="register" method="post">


<!-- Input and Submit elements -->
</form>

Many of the views in the Views/Account folder (generated when you create a new web
app with Individual User Accounts) contain the asp-route-returnurl attribute:

CSHTML

<form asp-controller="Account" asp-action="Login"


asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post" class="form-horizontal" role="form">

7 Note

With the built in templates, returnUrl is only populated automatically when you try
to access an authorized resource but are not authenticated or authorized. When
you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
The Form Action Tag Helper
The Form Action Tag Helper generates the formaction attribute on the generated
<button ...> or <input type="image" ...> tag. The formaction attribute controls where

a form submits its data. It binds to <input> elements of type image and <button>
elements. The Form Action Tag Helper enables the usage of several AnchorTagHelper
asp- attributes to control what formaction link is generated for the corresponding
element.

Supported AnchorTagHelper attributes to control the value of formaction :

Attribute Description

asp-controller The name of the controller.

asp-action The name of the action method.

asp-area The name of the area.

asp-page The name of the Razor page.

asp-page-handler The name of the Razor page handler.

asp-route The name of the route.

asp-route-{value} A single URL route value. For example, asp-route-id="1234" .

asp-all-route-data All route values.

asp-fragment The URL fragment.

Submit to controller example


The following markup submits the form to the Index action of HomeController when the
input or button are selected:

CSHTML

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

The previous markup generates following HTML:


HTML

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Submit to page example


The following markup submits the form to the About Razor Page:

CSHTML

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Submit to route example


Consider the /Home/Test endpoint:

C#

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

The following markup submits the form to the /Home/Test endpoint.

CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

The previous markup generates following HTML:

HTML

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

The Input Tag Helper


The Input Tag Helper binds an HTML <input> element to a model expression in your
razor view.

Syntax:

CSHTML

<input asp-for="<Expression Name>">

The Input Tag Helper:

Generates the id and name HTML attributes for the expression name specified in
the asp-for attribute. asp-for="Property1.Property2" is equivalent to m =>
m.Property1.Property2 . The name of the expression is what is used for the asp-for

attribute value. See the Expression names section for additional information.

Sets the HTML type attribute value based on the model type and data annotation
attributes applied to the model property

Won't overwrite the HTML type attribute value when one is specified

Generates HTML5 validation attributes from data annotation attributes applied


to model properties

Has an HTML Helper feature overlap with Html.TextBoxFor and Html.EditorFor .


See the HTML Helper alternatives to Input Tag Helper section for details.
Provides strong typing. If the name of the property changes and you don't update
the Tag Helper you'll get an error similar to the following:

An error occurred during the compilation of a resource required to


process
this request. Please review the following specific error details and
modify
your source code appropriately.

Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type
'RegisterViewModel'
could be found (are you missing a using directive or an assembly
reference?)

The Input Tag Helper sets the HTML type attribute based on the .NET type. The
following table lists some common .NET types and generated HTML type (not every
.NET type is listed).

.NET type Input Type

Bool type="checkbox"

String type="text"

DateTime type="datetime-local"

Byte type="number"

Int type="number"

Single, Double type="number"

The following table shows some common data annotations attributes that the input tag
helper will map to specific input types (not every validation attribute is listed):

Attribute Input Type

[EmailAddress] type="email"

[Url] type="url"

[HiddenInput] type="hidden"

[Phone] type="tel"
Attribute Input Type

[DataType(DataType.Password)] type="password"

[DataType(DataType.Date)] type="date"

[DataType(DataType.Time)] type="time"

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
<button type="submit">Register</button>
</form>

The code above generates the following HTML:

HTML

<form method="post" action="/Demo/RegisterInput">


Email:
<input type="email" data-val="true"
data-val-email="The Email Address field is not a valid email
address."
data-val-required="The Email Address field is required."
id="Email" name="Email" value=""><br>
Password:
<input type="password" data-val="true"
data-val-required="The Password field is required."
id="Password" name="Password"><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

The data annotations applied to the Email and Password properties generate metadata
on the model. The Input Tag Helper consumes the model metadata and produces
HTML5 data-val-* attributes (see Model Validation). These attributes describe the

validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" ,
where rule is the name of the validation rule (such as data-val-required , data-val-
email , data-val-maxlength , etc.) If an error message is provided in the attribute, it's

displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details
about the rule, for example, data-val-maxlength-max="1024" .

When binding multiple input controls to the same property, the generated controls
share the same id , which makes the generated mark-up invalid. To prevent duplicates,
specify the id attribute for each control explicitly.

Checkbox hidden input rendering


Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default
value to be sent for an unchecked checkbox, the Input Tag Helper generates an
additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a
boolean model property IsChecked :

CSHTML

<form method="post">
<input asp-for="@Model.IsChecked" />
<button type="submit">Submit</button>
</form>

The preceding Razor markup generates HTML markup similar to the following:

HTML
<form method="post">
<input name="IsChecked" type="checkbox" value="true" />
<button type="submit">Submit</button>

<input name="IsChecked" type="hidden" value="false" />


</form>

The preceding HTML markup shows an additional hidden input with a name of
IsChecked and a value of false . By default, this hidden input is rendered at the end of
the form. When the form is submitted:

If the IsChecked checkbox input is checked, both true and false are submitted as
values.
If the IsChecked checkbox input is unchecked, only the hidden input value false is
submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a
bool value, which results in true for checked checkboxes and false for unchecked
checkboxes.

To configure the behavior of the hidden input rendering, set the


CheckBoxHiddenInputRenderMode property on MvcViewOptions.HtmlHelperOptions.
For example:

C#

services.Configure<MvcViewOptions>(options =>
options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
CheckBoxHiddenInputRenderMode.None);

The preceding code disables hidden input rendering for checkboxes by setting
CheckBoxHiddenInputRenderMode to CheckBoxHiddenInputRenderMode.None. For all

available rendering modes, see the CheckBoxHiddenInputRenderMode enum.

HTML Helper alternatives to Input Tag Helper


Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping

features with the Input Tag Helper. The Input Tag Helper will automatically set the type
attribute; Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor
handle collections, complex objects and templates; the Input Tag Helper doesn't. The
Input Tag Helper, Html.EditorFor and Html.TextBoxFor are strongly typed (they use
lambda expressions); Html.TextBox and Html.Editor are not (they use expression
names).

HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named

htmlAttributes when executing their default templates. This behavior is optionally


augmented using additionalViewData parameters. The key "htmlAttributes" is case-
insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .

CSHTML

@Html.EditorFor(model => model.YourProperty,


new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })

Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda
expression. Therefore, asp-for="Property1" becomes m => m.Property1 in the
generated code which is why you don't need to prefix with Model . You can use the "@"
character to start an inline expression and move before the m. :

CSHTML

@{
var joe = "Joe";
}

<input asp-for="@joe">

Generates the following:

HTML

<input type="text" id="joe" name="joe" value="Joe">

With collection properties, asp-for="CollectionProperty[23].Member" generates the


same name as asp-for="CollectionProperty[i].Member" when i has the value 23 .

When ASP.NET Core MVC calculates the value of ModelExpression , it inspects several
sources, including ModelState . Consider <input type="text" asp-for="Name"> . The
calculated value attribute is the first non-null value from:

ModelState entry with key "Name".


Result of the expression Model.Name .

Navigating child properties


You can also navigate to child properties using the property path of the view model.
Consider a more complex model class that contains a child Address property.

C#

public class AddressViewModel


{
public string AddressLine1 { get; set; }
}

C#

public class RegisterAddressViewModel


{
public string Email { get; set; }

[DataType(DataType.Password)]
public string Password { get; set; }

public AddressViewModel Address { get; set; }


}

In the view, we bind to Address.AddressLine1 :

CSHTML

@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">


Email: <input asp-for="Email" /> <br />
Password: <input asp-for="Password" /><br />
Address: <input asp-for="Address.AddressLine1" /><br />
<button type="submit">Register</button>
</form>

The following HTML is generated for Address.AddressLine1 :

HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">

Expression names and Collections


Sample, a model containing an array of Colors :

C#

public class Person


{
public List<string> Colors { get; set; }

public int Age { get; set; }


}

The action method:

C#

public IActionResult Edit(int id, int colorIndex)


{
ViewData["Index"] = colorIndex;
return View(GetPerson(id));
}

The following Razor shows how you access a specific Color element:

CSHTML

@model Person
@{
var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">


@Html.EditorFor(m => m.Colors[index])
<label asp-for="Age"></label>
<input asp-for="Age" /><br />
<button type="submit">Post</button>
</form>

The Views/Shared/EditorTemplates/String.cshtml template:

CSHTML
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />

Sample using List<T> :

C#

public class ToDoItem


{
public string Name { get; set; }

public bool IsDone { get; set; }


}

The following Razor shows how to iterate over a collection:

CSHTML

@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">


<table>
<tr> <th>Name</th> <th>Is Done</th> </tr>

@for (int i = 0; i < Model.Count; i++)


{
<tr>
@Html.EditorFor(model => model[i])
</tr>
}

</table>
<button type="submit">Save</button>
</form>

The Views/Shared/EditorTemplates/ToDoItem.cshtml template:

CSHTML

@model ToDoItem

<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>

@*
This template replaces the following Razor which evaluates the indexer
three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@

foreach should be used if possible when the value is going to be used in an asp-for or
Html.DisplayFor equivalent context. In general, for is better than foreach (if the

scenario allows it) because it doesn't need to allocate an enumerator; however,


evaluating an indexer in a LINQ expression can be expensive and should be minimized.

7 Note

The commented sample code above shows how you would replace the lambda
expression with the @ operator to access each ToDoItem in the list.

The Textarea Tag Helper


The Textarea Tag Helper tag helper is similar to the Input Tag Helper.

Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.

Provides strong typing.

HTML Helper alternative: Html.TextAreaFor

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}

CSHTML

@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">


<textarea asp-for="Description"></textarea>
<button type="submit">Test</button>
</form>

The following HTML is generated:

HTML

<form method="post" action="/Demo/RegisterTextArea">


<textarea data-val="true"
data-val-maxlength="The field Description must be a string or array type
with a maximum length of &#x27;1024&#x27;."
data-val-maxlength-max="1024"
data-val-minlength="The field Description must be a string or array type
with a minimum length of &#x27;5&#x27;."
data-val-minlength-min="5"
id="Description" name="Description">
</textarea>
<button type="submit">Test</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Label Tag Helper


Generates the label caption and for attribute on a <label> element for an
expression name

HTML Helper alternative: Html.LabelFor .

The Label Tag Helper provides the following benefits over a pure HTML label element:

You automatically get the descriptive label value from the Display attribute. The
intended display name might change over time, and the combination of Display
attribute and Label Tag Helper will apply the Display everywhere it's used.
Less markup in source code

Strong typing with the model property.

Sample:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}

CSHTML

@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">


<label asp-for="Email"></label>
<input asp-for="Email" /> <br />
</form>

The following HTML is generated for the <label> element:

HTML

<label for="Email">Email Address</label>

The Label Tag Helper generated the for attribute value of "Email", which is the ID
associated with the <input> element. The Tag Helpers generate consistent id and for
elements so they can be correctly associated. The caption in this sample comes from the
Display attribute. If the model didn't contain a Display attribute, the caption would be
the property name of the expression. To override the default caption, add a caption
inside the label tag.

The Validation Tag Helpers


There are two Validation Tag Helpers. The Validation Message Tag Helper (which
displays a validation message for a single property on your model), and the Validation
Summary Tag Helper (which displays a summary of validation errors). The Input Tag

Helper adds HTML5 client side validation attributes to input elements based on data
annotation attributes on your model classes. Validation is also performed on the server.
The Validation Tag Helper displays these error messages when a validation error occurs.

The Validation Message Tag Helper


Adds the HTML5 data-valmsg-for="property" attribute to the span element,
which attaches the validation error messages on the input field of the specified
model property. When a client side validation error occurs, jQuery displays the
error message in the <span> element.

Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.

HTML Helper alternative: Html.ValidationMessageFor

The Validation Message Tag Helper is used with the asp-validation-for attribute on a
HTML span element.

CSHTML

<span asp-validation-for="Email"></span>

The Validation Message Tag Helper will generate the following HTML:

HTML

<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>

You generally use the Validation Message Tag Helper after an Input Tag Helper for the
same property. Doing so displays any validation error messages near the input that
caused the error.

7 Note

You must have a view with the correct JavaScript and jQuery script references in
place for client side validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server
side validation or client-side validation is disabled), MVC places that error message as
the body of the <span> element.

HTML

<span class="field-validation-error" data-valmsg-for="Email"


data-valmsg-replace="true">
The Email Address field is required.
</span>

The Validation Summary Tag Helper


Targets <div> elements with the asp-validation-summary attribute

HTML Helper alternative: @Html.ValidationSummary

The Validation Summary Tag Helper is used to display a summary of validation


messages. The asp-validation-summary attribute value can be any of the following:

asp-validation-summary Validation messages displayed

All Property and model level

ModelOnly Model

None None

Sample
In the following example, the data model has DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error
occurs, the Validation Tag Helper displays the error message:

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

CSHTML

@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">


<div asp-validation-summary="ModelOnly"></div>
Email: <input asp-for="Email" /> <br />
<span asp-validation-for="Email"></span><br />
Password: <input asp-for="Password" /><br />
<span asp-validation-for="Password"></span><br />
<button type="submit">Register</button>
</form>

The generated HTML (when the model is valid):

HTML

<form action="/DemoReg/Register" method="post">


Email: <input name="Email" id="Email" type="email" value=""
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address."
data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Email"></span><br>
Password: <input name="Password" id="Password" type="password"
data-val-required="The Password field is required." data-val="true"><br>
<span class="field-validation-valid" data-valmsg-replace="true"
data-valmsg-for="Password"></span><br>
<button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>

The Select Tag Helper


Generates select and associated option elements for properties of your model.

Has an HTML Helper alternative Html.DropDownListFor and Html.ListBoxFor

The Select Tag Helper asp-for specifies the model property name for the select
element and asp-items specifies the option elements. For example:
CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>

Sample:

C#

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
};
}
}

The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.

C#

public IActionResult Index()


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

The HTTP POST Index method displays the selection:

C#

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}

// If we got this far, something failed; redisplay form.


return View(model);
}

The Index view:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">


<select asp-for="Country" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Which generates the following HTML (with "CA" selected):

HTML

<form method="post" action="/">


<select id="Country" name="Country">
<option value="MX">Mexico</option>
<option selected="selected" value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

7 Note

We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view
model is more robust at providing MVC metadata and generally less problematic.

The asp-for attribute value is a special case and doesn't require a Model prefix, the
other Tag Helper attributes do (such as asp-items )

CSHTML

<select asp-for="Country" asp-items="Model.Countries"></select>


Enum binding
It's often convenient to use <select> with an enum property and generate the
SelectListItem elements from the enum values.

Sample:

C#

public class CountryEnumViewModel


{
public CountryEnum EnumCountry { get; set; }
}
}

C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The GetEnumSelectList method generates a SelectList object for an enum.

CSHTML

@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">


<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
<br /><button type="submit">Register</button>
</form>

You can mark your enumerator list with the Display attribute to get a richer UI:
C#

using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}

The following HTML is generated:

HTML

<form method="post" action="/Home/IndexEnum">


<select data-val="true" data-val-required="The EnumCountry field is
required."
id="EnumCountry" name="EnumCountry">
<option value="0">United Mexican States</option>
<option value="1">United States of America</option>
<option value="2">Canada</option>
<option value="3">France</option>
<option value="4">Germany</option>
<option selected="selected" value="5">Spain</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="
<removed for brevity>">
</form>

Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.

The CountryViewModelGroup groups the SelectListItem elements into the "North


America" and "Europe" groups:

C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };

Countries = new List<SelectListItem>


{
new SelectListItem
{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "CAN",
Text = "Canada",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "US",
Text = "USA",
Group = NorthAmericaGroup
},
new SelectListItem
{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
new SelectListItem
{
Value = "ES",
Text = "Spain",
Group = EuropeGroup
},
new SelectListItem
{
Value = "DE",
Text = "Germany",
Group = EuropeGroup
}
};
}

public string Country { get; set; }

public List<SelectListItem> Countries { get; }


The two groups are shown below:

The generated HTML:

HTML

<form method="post" action="/Home/IndexGroup">


<select id="Country" name="Country">
<optgroup label="North America">
<option value="MEX">Mexico</option>
<option value="CAN">Canada</option>
<option value="US">USA</option>
</optgroup>
<optgroup label="Europe">
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</optgroup>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if
the property specified in the asp-for attribute is an IEnumerable . For example, given the
following model:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new


List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
new SelectListItem { Value = "US", Text = "USA" },
new SelectListItem { Value = "FR", Text = "France" },
new SelectListItem { Value = "ES", Text = "Spain" },
new SelectListItem { Value = "DE", Text = "Germany"}
};
}
}

With the following view:

CSHTML

@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">


<select asp-for="CountryCodes" asp-items="Model.Countries"></select>
<br /><button type="submit">Register</button>
</form>

Generates the following HTML:

HTML

<form method="post" action="/Home/IndexMultiSelect">


<select id="CountryCodes"
multiple="multiple"
name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed for
brevity>">
</form>
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a
template to eliminate repeating the HTML:

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


@Html.EditorForModel()
<br /><button type="submit">Register</button>
</form>

The Views/Shared/EditorTemplates/CountryViewModel.cshtml template:

CSHTML

@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">


<option value="">--none--</option>
</select>

Adding HTML <option> elements isn't limited to the No selection case. For example,
the following view and action method will generate HTML similar to the code above:

C#

public IActionResult IndexNone()


{
var model = new CountryViewModel();
model.Countries.Insert(0, new SelectListItem("<none>", ""));
return View(model);
}

CSHTML

@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">


<select asp-for="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
</form>

The correct <option> element will be selected ( contain the selected="selected"


attribute) depending on the current Country value.

C#

public IActionResult IndexOption(int id)


{
var model = new CountryViewModel();
model.Country = "CA";
return View(model);
}

HTML

<form method="post" action="/Home/IndexEmpty">


<select id="Country" name="Country">
<option value="">&lt;none&gt;</option>
<option value="MX">Mexico</option>
<option value="CA" selected="selected">Canada</option>
<option value="US">USA</option>
</select>
<br /><button type="submit">Register</button>
<input name="__RequestVerificationToken" type="hidden" value="<removed
for brevity>">
</form>

Additional resources
Tag Helpers in ASP.NET Core
HTML Form element
Request Verification Token
Model Binding in ASP.NET Core
Model validation in ASP.NET Core MVC
IAttributeAdapter Interface
Code snippets for this document
Share controllers, views, Razor Pages
and more with Application Parts
Article • 06/03/2022

By Rick Anderson

View or download sample code (how to download)

An Application Part is an abstraction over the resources of an app. Application Parts


allow ASP.NET Core to discover controllers, view components, tag helpers, Razor Pages,
razor compilation sources, and more. AssemblyPart is an Application part. AssemblyPart
encapsulates an assembly reference and exposes types and compilation references.

Feature providers work with application parts to populate the features of an ASP.NET
Core app. The main use case for application parts is to configure an app to discover (or
avoid loading) ASP.NET Core features from an assembly. For example, you might want
to share common functionality between multiple apps. Using Application Parts, you can
share an assembly (DLL) containing controllers, views, Razor Pages, razor compilation
sources, Tag Helpers, and more with multiple apps. Sharing an assembly is preferred to
duplicating code in multiple projects.

ASP.NET Core apps load features from ApplicationPart. The AssemblyPart class
represents an application part that's backed by an assembly.

Load ASP.NET Core features


Use the Microsoft.AspNetCore.Mvc.ApplicationParts and AssemblyPart classes to
discover and load ASP.NET Core features (controllers, view components, etc.). The
ApplicationPartManager tracks the application parts and feature providers available.
ApplicationPartManager is configured in Startup.ConfigureServices :

C#

// Requires using System.Reflection;


public void ConfigureServices(IServiceCollection services)
{
var assembly = typeof(MySharedController).Assembly;
services.AddControllersWithViews()
.AddApplicationPart(assembly)
.AddRazorRuntimeCompilation();

services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
{ options.FileProviders.Add(new EmbeddedFileProvider(assembly)); });
}

The following code provides an alternative approach to configuring


ApplicationPartManager using AssemblyPart :

C#

// Requires using System.Reflection;


// Requires using Microsoft.AspNetCore.Mvc.ApplicationParts;
public void ConfigureServices(IServiceCollection services)
{
var assembly = typeof(MySharedController).Assembly;
// This creates an AssemblyPart, but does not create any related parts
for items such as views.
var part = new AssemblyPart(assembly);
services.AddControllersWithViews()
.ConfigureApplicationPartManager(apm =>
apm.ApplicationParts.Add(part));
}

The preceding two code samples load the SharedController from an assembly. The
SharedController is not in the app's project. See the WebAppParts solution sample
download.

Include views
Use a Razor class library to include views in the assembly.

Prevent loading resources


Application parts can be used to avoid loading resources in a particular assembly or
location. Add or remove members of the Microsoft.AspNetCore.Mvc.ApplicationParts
collection to hide or make available resources. The order of the entries in the
ApplicationParts collection isn't important. Configure the ApplicationPartManager
before using it to configure services in the container. For example, configure the
ApplicationPartManager before invoking AddControllersAsServices . Call Remove on the

ApplicationParts collection to remove a resource.

The ApplicationPartManager includes parts for:

The app's assembly and dependent assemblies.


Microsoft.AspNetCore.Mvc.ApplicationParts.CompiledRazorAssemblyPart

Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
Microsoft.AspNetCore.Mvc.TagHelpers .

Microsoft.AspNetCore.Mvc.Razor .

Feature providers
Application feature providers examine application parts and provide features for those
parts. There are built-in feature providers for the following ASP.NET Core features:

ControllerFeatureProvider
TagHelperFeatureProvider
MetadataReferenceFeatureProvider
ViewsFeatureProvider
internal class RazorCompiledItemFeatureProvider

Feature providers inherit from IApplicationFeatureProvider<TFeature>, where T is the


type of the feature. Feature providers can be implemented for any of the previously
listed feature types. The order of feature providers in the
ApplicationPartManager.FeatureProviders can impact run time behavior. Later added
providers can react to actions taken by earlier added providers.

Display available features


The features available to an app can be enumerated by requesting an
ApplicationPartManager through dependency injection:

C#

using AppPartsSample.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewComponents;

namespace AppPartsSample.Controllers
{
public class FeaturesController : Controller
{
private readonly ApplicationPartManager _partManager;

public FeaturesController(ApplicationPartManager partManager)


{
_partManager = partManager;
}
public IActionResult Index()
{
var viewModel = new FeaturesViewModel();

var controllerFeature = new ControllerFeature();


_partManager.PopulateFeature(controllerFeature);
viewModel.Controllers = controllerFeature.Controllers.ToList();

var tagHelperFeature = new TagHelperFeature();


_partManager.PopulateFeature(tagHelperFeature);
viewModel.TagHelpers = tagHelperFeature.TagHelpers.ToList();

var viewComponentFeature = new ViewComponentFeature();


_partManager.PopulateFeature(viewComponentFeature);
viewModel.ViewComponents =
viewComponentFeature.ViewComponents.ToList();

return View(viewModel);
}
}
}

The download sample uses the preceding code to display the app features:

text

Controllers:
- FeaturesController
- HomeController
- HelloController
- GenericController`1
- GenericController`1
Tag Helpers:
- PrerenderTagHelper
- AnchorTagHelper
- CacheTagHelper
- DistributedCacheTagHelper
- EnvironmentTagHelper
- Additional Tag Helpers omitted for brevity.
View Components:
- SampleViewComponent

Discovery in application parts


HTTP 404 errors are not uncommon when developing with application parts. These
errors are typically caused by missing an essential requirement for how applications
parts are discovered. If your app returns an HTTP 404 error, verify the following
requirements have been met:
The applicationName setting needs to be set to the root assembly used for
discovery. The root assembly used for discovery is normally the entry point
assembly.
The root assembly needs to have a reference to the parts used for discovery. The
reference can be direct or transitive.
The root assembly needs to reference the Web SDK. The framework has logic that
stamps attributes into the root assembly that are used for discovery.
Work with the application model in
ASP.NET Core
Article • 06/03/2022

By Steve Smith

ASP.NET Core MVC defines an application model representing the components of an


MVC app. Read and manipulate this model to modify how MVC elements behave. By
default, MVC follows certain conventions to determine which classes are considered
controllers, which methods on those classes are actions, and how parameters and
routing behave. Customize this behavior to suit an app's needs by creating custom
conventions and applying them globally or as attributes.

Models and Providers


( IApplicationModelProvider )
The ASP.NET Core MVC application model includes both abstract interfaces and
concrete implementation classes that describe an MVC application. This model is the
result of MVC discovering the app's controllers, actions, action parameters, routes, and
filters according to default conventions. By working with the application model, modify
an app to follow different conventions from the default MVC behavior. The parameters,
names, routes, and filters are all used as configuration data for actions and controllers.

The ASP.NET Core MVC Application Model has the following structure:

ApplicationModel
Controllers (ControllerModel)
Actions (ActionModel)
Parameters (ParameterModel)

Each level of the model has access to a common Properties collection, and lower levels
can access and overwrite property values set by higher levels in the hierarchy. The
properties are persisted to the ActionDescriptor.Properties when the actions are created.
Then when a request is being handled, any properties a convention added or modified
can be accessed through ActionContext.ActionDescriptor. Using properties is a great
way to configure filters, model binders, and other app model aspects on a per-action
basis.

7 Note
The ActionDescriptor.Properties collection isn't thread safe (for writes) after app
startup. Conventions are the best way to safely add data to this collection.

ASP.NET Core MVC loads the application model using a provider pattern, defined by the
IApplicationModelProvider interface. This section covers some of the internal
implementation details of how this provider functions. Use of the provider pattern is an
advanced subject, primarily for framework use. Most apps should use conventions, not
the provider pattern.

Implementations of the IApplicationModelProvider interface "wrap" one another, where


each implementation calls OnProvidersExecuting in ascending order based on its Order
property. The OnProvidersExecuted method is then called in reverse order. The
framework defines several providers:

First ( Order=-1000 ):

DefaultApplicationModelProvider

Then ( Order=-990 ):

AuthorizationApplicationModelProvider

CorsApplicationModelProvider

7 Note

The order in which two providers with the same value for Order are called is
undefined and shouldn't be relied upon.

7 Note

IApplicationModelProvider is an advanced concept for framework authors to


extend. In general, apps should use conventions, and frameworks should use
providers. The key distinction is that providers always run before conventions.

The DefaultApplicationModelProvider establishes many of the default behaviors used by


ASP.NET Core MVC. Its responsibilities include:

Adding global filters to the context


Adding controllers to the context
Adding public controller methods as actions
Adding action method parameters to the context
Applying route and other attributes

Some built-in behaviors are implemented by the DefaultApplicationModelProvider . This


provider is responsible for constructing the ControllerModel, which in turn references
ActionModel, PropertyModel, and ParameterModel instances. The
DefaultApplicationModelProvider class is an internal framework implementation detail

that may change in the future.

The AuthorizationApplicationModelProvider is responsible for applying the behavior


associated with the AuthorizeFilter and AllowAnonymousFilter attributes. For more
information, see Simple authorization in ASP.NET Core.

The CorsApplicationModelProvider implements behavior associated with


IEnableCorsAttribute and IDisableCorsAttribute. For more information, see Enable Cross-
Origin Requests (CORS) in ASP.NET Core.

Information on the framework's internal providers described in this section aren't


available via the .NET API browser. However, the providers may be inspected in the
ASP.NET Core reference source (dotnet/aspnetcore GitHub repository) . Use GitHub
search to find the providers by name and select the version of the source with the
Switch branches/tags dropdown list.

Conventions
The application model defines convention abstractions that provide a simpler way to
customize the behavior of the models than overriding the entire model or provider.
These abstractions are the recommended way to modify an app's behavior. Conventions
provide a way to write code that dynamically applies customizations. While filters
provide a means of modifying the framework's behavior, customizations permit control
over how the whole app works together.

The following conventions are available:

IApplicationModelConvention
IControllerModelConvention
IActionModelConvention
IParameterModelConvention

Conventions are applied by adding them to MVC options or by implementing attributes


and applying them to controllers, actions, or action parameters (similar to filters).Unlike
filters, conventions are only executed when the app is starting, not as part of each
request.
7 Note

For information on Razor Pages route and application model provider conventions,
see Razor Pages route and app conventions in ASP.NET Core.

Modify the ApplicationModel


The following convention is used to add a property to the application model:

C#

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class ApplicationDescription : IApplicationModelConvention
{
private readonly string _description;

public ApplicationDescription(string description)


{
_description = description;
}

public void Apply(ApplicationModel application)


{
application.Properties["description"] = _description;
}
}
}

Application model conventions are applied as options when MVC is added in


Startup.ConfigureServices :

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Conventions.Add(new ApplicationDescription("My Application
Description"));
options.Conventions.Add(new NamespaceRoutingConvention());
});
}
Properties are accessible from the ActionDescriptor.Properties collection within
controller actions:

C#

public class AppModelController : Controller


{
public string Description()
{
return "Description: " +
ControllerContext.ActionDescriptor.Properties["description"];
}
}

Modify the ControllerModel description


The controller model can also include custom properties. Custom properties override
existing properties with the same name specified in the application model. The following
convention attribute adds a description at the controller level:

C#

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class ControllerDescriptionAttribute : Attribute,
IControllerModelConvention
{
private readonly string _description;

public ControllerDescriptionAttribute(string description)


{
_description = description;
}

public void Apply(ControllerModel controllerModel)


{
controllerModel.Properties["description"] = _description;
}
}
}

This convention is applied as an attribute on a controller:

C#
[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
public string Index()
{
return "Description: " +
ControllerContext.ActionDescriptor.Properties["description"];
}

Modify the ActionModel description


A separate attribute convention can be applied to individual actions, overriding behavior
already applied at the application or controller level:

C#

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class ActionDescriptionAttribute : Attribute,
IActionModelConvention
{
private readonly string _description;

public ActionDescriptionAttribute(string description)


{
_description = description;
}

public void Apply(ActionModel actionModel)


{
actionModel.Properties["description"] = _description;
}
}
}

Applying this to an action within the controller demonstrates how it overrides the
controller-level convention:

C#

[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
public string Index()
{
return "Description: " +
ControllerContext.ActionDescriptor.Properties["description"];
}

[ActionDescription("Action Description")]
public string UseActionDescriptionAttribute()
{
return "Description: " +
ControllerContext.ActionDescriptor.Properties["description"];
}
}

Modify the ParameterModel


The following convention can be applied to action parameters to modify their
BindingInfo. The following convention requires that the parameter be a route parameter.
Other potential binding sources, such as query string values, are ignored:

C#

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace AppModelSample.Conventions
{
public class MustBeInRouteParameterModelConvention : Attribute,
IParameterModelConvention
{
public void Apply(ParameterModel model)
{
if (model.BindingInfo == null)
{
model.BindingInfo = new BindingInfo();
}
model.BindingInfo.BindingSource = BindingSource.Path;
}
}
}

The attribute may be applied to any action parameter:

C#

public class ParameterModelController : Controller


{
// Will bind: /ParameterModel/GetById/123
// WON'T bind: /ParameterModel/GetById?id=123
public string GetById([MustBeInRouteParameterModelConvention]int id)
{
return $"Bound to id: {id}";
}
}

To apply the convention to all action parameters, add the


MustBeInRouteParameterModelConvention to MvcOptions in Startup.ConfigureServices :

C#

options.Conventions.Add(new MustBeInRouteParameterModelConvention());

Modify the ActionModel name


The following convention modifies the ActionModel to update the name of the action to
which it's applied. The new name is provided as a parameter to the attribute. This new
name is used by routing, so it affects the route used to reach this action method:

C#

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class CustomActionNameAttribute : Attribute,
IActionModelConvention
{
private readonly string _actionName;

public CustomActionNameAttribute(string actionName)


{
_actionName = actionName;
}

public void Apply(ActionModel actionModel)


{
// this name will be used by routing
actionModel.ActionName = _actionName;
}
}
}

This attribute is applied to an action method in the HomeController :

C#
// Route: /Home/MyCoolAction
[CustomActionName("MyCoolAction")]
public string SomeName()
{
return ControllerContext.ActionDescriptor.ActionName;
}

Even though the method name is SomeName , the attribute overrides the MVC convention
of using the method name and replaces the action name with MyCoolAction . Thus, the
route used to reach this action is /Home/MyCoolAction .

7 Note

This example in this section is essentially the same as using the built-in
ActionNameAttribute.

Custom routing convention


Use an IApplicationModelConvention to customize how routing works. For example, the
following convention incorporates controllers' namespaces into their routes, replacing .
in the namespace with / in the route:

C#

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;

namespace AppModelSample.Conventions
{
public class NamespaceRoutingConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
var hasAttributeRouteModels = controller.Selectors
.Any(selector => selector.AttributeRouteModel != null);

if (!hasAttributeRouteModels
&& controller.ControllerName.Contains("Namespace")) //
affect one controller in this sample
{
// Replace the . in the namespace with a / to create the
attribute route
// Ex: MySite.Admin namespace will correspond to
MySite/Admin attribute route
// Then attach [controller], [action] and optional {id?}
token.
// [Controller] and [action] is replaced with the
controller and action
// name to generate the final template
controller.Selectors[0].AttributeRouteModel = new
AttributeRouteModel()
{
Template =
controller.ControllerType.Namespace.Replace('.', '/') +
"/[controller]/[action]/{id?}"
};
}
}

// You can continue to put attribute route templates for the


controller actions depending on the way you want them to behave
}
}
}

The convention is added as an option in Startup.ConfigureServices :

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Conventions.Add(new ApplicationDescription("My Application
Description"));
options.Conventions.Add(new NamespaceRoutingConvention());
});
}

 Tip

Add conventions to middleware via MvcOptions using the following approach. The
{CONVENTION} placeholder is the convention to add:

C#

services.Configure<MvcOptions>(c => c.Conventions.Add({CONVENTION}));

The following example applies a convention to routes that aren't using attribute routing
where the controller has Namespace in its name:
C#

using Microsoft.AspNetCore.Mvc;

namespace AppModelSample.Controllers
{
public class NamespaceRoutingController : Controller
{
// using NamespaceRoutingConvention
// route: /AppModelSample/Controllers/NamespaceRouting/Index
public string Index()
{
return "This demonstrates namespace routing.";
}
}
}

Use ApiExplorer to document an app


The application model exposes an ApiExplorerModel property at each level that can be
used to traverse the app's structure. This can be used to generate help pages for web
APIs using tools like Swagger. The ApiExplorer property exposes an IsVisible property
that can be set to specify which parts of the app's model should be exposed. Configure
this setting using a convention:

C#

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
public class EnableApiExplorerApplicationConvention :
IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
application.ApiExplorer.IsVisible = true;
}
}
}

Using this approach (and additional conventions if required), API visibility is enabled or
disabled at any level within an app.
Areas in ASP.NET Core
Article • 06/03/2022

By Dhananjay Kumar and Rick Anderson

Areas are an ASP.NET feature used to organize related functionality into a group as a
separate:

Namespace for routing.


Folder structure for views and Razor Pages.

Using areas creates a hierarchy for the purpose of routing by adding another route
parameter, area , to controller and action or a Razor Page page .

Areas provide a way to partition an ASP.NET Core Web app into smaller functional
groups, each with its own set of Razor Pages, controllers, views, and models. An area is
effectively a structure inside an app. In an ASP.NET Core web project, logical
components like Pages, Model, Controller, and View are kept in different folders. The
ASP.NET Core runtime uses naming conventions to create the relationship between
these components. For a large app, it may be advantageous to partition the app into
separate high level areas of functionality. For instance, an e-commerce app with multiple
business units, such as checkout, billing, and search. Each of these units have their own
area to contain views, controllers, Razor Pages, and models.

Consider using Areas in a project when:

The app is made of multiple high-level functional components that can be logically
separated.
You want to partition the app so that each functional area can be worked on
independently.

If you're using Razor Pages, see Areas with Razor Pages in this document.

Areas for controllers with views


A typical ASP.NET Core web app using areas, controllers, and views contains the
following:

An Area folder structure.

Controllers with the [Area] attribute to associate the controller with the area:

C#
[Area("Products")]
public class ManageController : Controller
{

The area route added to Program.cs:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
name: "MyArea",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Area folder structure


Consider an app that has two logical groups, Products and Services. Using areas, the
folder structure would be similar to the following:

Project name
Areas
Products
Controllers
HomeController.cs
ManageController.cs
Views
Home
Index.cshtml
Manage
Index.cshtml
About.cshtml
Services
Controllers
HomeController.cs
Views
Home
Index.cshtml

While the preceding layout is typical when using Areas, only the view files are required
to use this folder structure. View discovery searches for a matching area view file in the
following order:

text

/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml
/Pages/Shared/<Action-Name>.cshtml

Associate the controller with an Area


Area controllers are designated with the [Area] attribute:

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.Docs.Samples;

namespace MVCareas.Areas.Products.Controllers;

[Area("Products")]
public class ManageController : Controller
{
public IActionResult Index()
{
ViewData["routeInfo"] = ControllerContext.MyDisplayRouteInfo();
return View();
}

public IActionResult About()


{
ViewData["routeInfo"] = ControllerContext.MyDisplayRouteInfo();
return View();
}
}

Add Area route


Area routes typically use conventional routing rather than attribute routing.
Conventional routing is order-dependent. In general, routes with areas should be placed
earlier in the route table as they're more specific than routes without an area.

{area:...} can be used as a token in route templates if url space is uniform across all
areas:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
name: "MyArea",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

In the preceding code, exists applies a constraint that the route must match an area.
Using {area:...} with MapControllerRoute :

Is the least complicated mechanism to adding routing to areas.


Matches all controllers with the [Area("Area name")] attribute.

The following code uses MapAreaControllerRoute to create two named area routes:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapAreaControllerRoute(
name: "MyAreaProducts",
areaName: "Products",
pattern: "Products/{controller=Home}/{action=Index}/{id?}");

app.MapAreaControllerRoute(
name: "MyAreaServices",
areaName: "Services",
pattern: "Services/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

For more information, see Area routing.

Link generation with MVC areas


The following code from the sample download shows link generation with the area
specified:

CSHTML
<li>Anchor Tag Helper links</li>
<ul>
<li>
<a asp-area="Products" asp-controller="Home" asp-action="About">
Products/Home/About
</a>
</li>
<li>
<a asp-area="Services" asp-controller="Home" asp-action="About">
Services About
</a>
</li>
<li>
<a asp-area="" asp-controller="Home" asp-action="About">
/Home/About
</a>
</li>
</ul>
<li>Html.ActionLink generated links</li>
<ul>
<li>
@Html.ActionLink("Product/Manage/About", "About", "Manage",
new { area = "Products" })
</li>
</ul>
<li>Url.Action generated links</li>
<ul>
<li>
<a href='@Url.Action("About", "Manage", new { area = "Products" })'>
Products/Manage/About
</a>
</li>
</ul>

The sample download includes a partial view that contains:

The preceding links.


Links similar to the preceding except area is not specified.

The partial view is referenced in the layout file, so every page in the app displays the
generated links. The links generated without specifying the area are only valid when
referenced from a page in the same area and controller.

When the area or controller is not specified, routing depends on the ambient values.
The current route values of the current request are considered ambient values for link
generation. In many cases for the sample app, using the ambient values generates
incorrect links with the markup that doesn't specify the area.

For more information, see Routing to controller actions.


Shared layout for Areas using the _ViewStart.cshtml file
To share a common layout for the entire app, keep the _ViewStart.cshtml in the
application root folder. For more information, see Layout in ASP.NET Core

Application root folder


The application root folder is the folder containing the Program.cs file in a web app
created with the ASP.NET Core templates.

_ViewImports.cshtml
/Views/_ViewImports.cshtml, for MVC, and /Pages/_ViewImports.cshtml for Razor Pages,
is not imported to views in areas. Use one of the following approaches to provide view
imports to all views:

Add _ViewImports.cshtml to the application root folder. A _ViewImports.cshtml in


the application root folder will apply to all views in the app.
Copy the _ViewImports.cshtml file to the appropriate view folder under areas. For
example, a Razor Pages app created with individual user accounts has a
_ViewImports.cshtml file in the following folders:
/Areas/Identity/Pages/_ViewImports.cshtml
/Pages/_ViewImports.cshtml

The _ViewImports.cshtml file typically contains Tag Helpers imports, @using , and @inject
statements. For more information, see Importing Shared Directives.

Change default area folder where views are stored


The following code changes the default area folder from "Areas" to "MyAreas" :

C#

using Microsoft.AspNetCore.Mvc.Razor;

var builder = WebApplication.CreateBuilder(args);


builder.Services.Configure<RazorViewEngineOptions>(options =>
{
options.AreaViewLocationFormats.Clear();

options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/{1}/{0}.cshtml");

options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
});

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
name: "MyArea",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Areas with Razor Pages


Areas with Razor Pages require an Areas/<area name>/Pages folder in the root of the
app. The following folder structure is used with the sample app :

Project name
Areas
Products
Pages
_ViewImports
About
Index
Services
Pages
Manage
About
Index

Link generation with Razor Pages and areas


The following code from the sample download shows link generation with the area
specified (for example, asp-area="Products" ):

CSHTML

<li>Anchor Tag Helper links</li>


<ul>
<li>
<a asp-area="Products" asp-page="/About">
Products/About
</a>
</li>
<li>
<a asp-area="Services" asp-page="/Manage/About">
Services/Manage/About
</a>
</li>
<li>
<a asp-area="" asp-page="/About">
/About
</a>
</li>
</ul>
<li>Url.Page generated links</li>
<ul>
<li>
<a href='@Url.Page("/Manage/About", new { area = "Services" })'>
Services/Manage/About
</a>
</li>
<li>
<a href='@Url.Page("/About", new { area = "Products" })'>
Products/About
</a>
</li>
</ul>

The sample download includes a partial view that contains the preceding links and the
same links without specifying the area. The partial view is referenced in the layout file, so
every page in the app displays the generated links. The links generated without
specifying the area are only valid when referenced from a page in the same area.

When the area is not specified, routing depends on the ambient values. The current
route values of the current request are considered ambient values for link generation. In
many cases for the sample app, using the ambient values generates incorrect links. For
example, consider the links generated from the following code:

CSHTML

<li>
<a asp-page="/Manage/About">
Services/Manage/About
</a>
</li>
<li>
<a asp-page="/About">
/About
</a>
</li>

For the preceding code:

The link generated from <a asp-page="/Manage/About"> is correct only when the
last request was for a page in Services area. For example, /Services/Manage/ ,
/Services/Manage/Index , or /Services/Manage/About .

The link generated from <a asp-page="/About"> is correct only when the last
request was for a page in /Home .
The code is from the sample download .

Import namespace and Tag Helpers with _ViewImports


file
A _ViewImports.cshtml file can be added to each area Pages folder to import the
namespace and Tag Helpers to each Razor Page in the folder.

Consider the Services area of the sample code, which doesn't contain a
_ViewImports.cshtml file. The following markup shows the /Services/Manage/About Razor
Page:

CSHTML

@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model RPareas.Areas.Services.Pages.Manage.AboutModel
@{
ViewData["Title"] = "Srv Mng About";
}

<div>
ViewData["routeInfo"]: @ViewData["routeInfo"]
</div>

<a asp-area="Products" asp-page="/Index">


Products/Index
</a>

In the preceding markup:

The fully qualified class name must be used to specify the model ( @model
RPareas.Areas.Services.Pages.Manage.AboutModel ).

Tag Helpers are enabled by @addTagHelper *,


Microsoft.AspNetCore.Mvc.TagHelpers

In the sample download, the Products area contains the following _ViewImports.cshtml
file:

CSHTML

@namespace RPareas.Areas.Products.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The following markup shows the /Products/About Razor Page:

CSHTML

@page
@model AboutModel
@{
ViewData["Title"] = "Prod About";
}

In the preceding file, the namespace and @addTagHelper directive is imported to the file
by the Areas/Products/Pages/_ViewImports.cshtml file.

For more information, see Managing Tag Helper scope and Importing Shared Directives.

Shared layout for Razor Pages Areas


To share a common layout for the entire app, move the _ViewStart.cshtml to the
application root folder.
Publishing Areas
All *.cshtml files and files within the wwwroot directory are published to output when
<Project Sdk="Microsoft.NET.Sdk.Web"> is included in the *.csproj file.

Add MVC Area with Visual Studio


In Solution Explorer, right click the project and select ADD > New Scaffolded Item, then
select MVC Area.

Additional resources
View or download sample code (how to download). The download sample
provides a basic app for testing areas.
MyDisplayRouteInfo and ToCtxString are provided by the
Rick.Docs.Samples.RouteInfo NuGet package. The methods display Controller
and Razor Page route information.
Filters in ASP.NET Core
Article • 06/20/2023

By Kirk Larkin , Rick Anderson , Tom Dykstra , and Steve Smith

Filters in ASP.NET Core allow code to run before or after specific stages in the request
processing pipeline.

Built-in filters handle tasks such as:

Authorization, preventing access to resources a user isn't authorized for.


Response caching, short-circuiting the request pipeline to return a cached
response.

Custom filters can be created to handle cross-cutting concerns. Examples of cross-


cutting concerns include error handling, caching, configuration, authorization, and
logging. Filters avoid duplicating code. For example, an error handling exception filter
could consolidate error handling.

This document applies to Razor Pages, API controllers, and controllers with views. Filters
don't work directly with Razor components. A filter can only indirectly affect a
component when:

The component is embedded in a page or view.


The page or controller and view uses the filter.

How filters work


Filters run within the ASP.NET Core action invocation pipeline, sometimes referred to as
the filter pipeline. The filter pipeline runs after ASP.NET Core selects the action to
execute:
Filter types
Each filter type is executed at a different stage in the filter pipeline:

Authorization filters:
Run first.
Determine whether the user is authorized for the request.
Short-circuit the pipeline if the request is not authorized.

Resource filters:
Run after authorization.
OnResourceExecuting runs code before the rest of the filter pipeline. For
example, OnResourceExecuting runs code before model binding.
OnResourceExecuted runs code after the rest of the pipeline has completed.

Action filters:
Run immediately before and after an action method is called.
Can change the arguments passed into an action.
Can change the result returned from the action.
Are not supported in Razor Pages.

Endpoint filters:
Run immediately before and after an action method is called.
Can change the arguments passed into an action.
Can change the result returned from the action.
Are not supported in Razor Pages.
Can be invoked on both actions and route handler-based endpoints.

Exception filters apply global policies to unhandled exceptions that occur before
the response body has been written to.

Result filters:
Run immediately before and after the execution of action results.
Run only when the action method executes successfully.
Are useful for logic that must surround view or formatter execution.

The following diagram shows how filter types interact in the filter pipeline:

Razor Pages also support Razor Page filters, which run before and after a Razor Page
handler.
Implementation
Filters support both synchronous and asynchronous implementations through different
interface definitions.

Synchronous filters run before and after their pipeline stage. For example,
OnActionExecuting is called before the action method is called. OnActionExecuted is
called after the action method returns:

C#

public class SampleActionFilter : IActionFilter


{
public void OnActionExecuting(ActionExecutingContext context)
{
// Do something before the action executes.
}

public void OnActionExecuted(ActionExecutedContext context)


{
// Do something after the action executes.
}
}

Asynchronous filters define an On-Stage-ExecutionAsync method. For example,


OnActionExecutionAsync:

C#

public class SampleAsyncActionFilter : IAsyncActionFilter


{
public async Task OnActionExecutionAsync(
ActionExecutingContext context, ActionExecutionDelegate next)
{
// Do something before the action executes.
await next();
// Do something after the action executes.
}
}

In the preceding code, the SampleAsyncActionFilter has an ActionExecutionDelegate,


next , which executes the action method.

Multiple filter stages


Interfaces for multiple filter stages can be implemented in a single class. For example,
the ActionFilterAttribute class implements:

Synchronous: IActionFilter and IResultFilter


Asynchronous: IAsyncActionFilter and IAsyncResultFilter
IOrderedFilter

Implement either the synchronous or the async version of a filter interface, not both.
The runtime checks first to see if the filter implements the async interface, and if so, it
calls that. If not, it calls the synchronous interface's method(s). If both asynchronous and
synchronous interfaces are implemented in one class, only the async method is called.
When using abstract classes like ActionFilterAttribute, override only the synchronous
methods or the asynchronous methods for each filter type.

Built-in filter attributes


ASP.NET Core includes built-in attribute-based filters that can be subclassed and
customized. For example, the following result filter adds a header to the response:

C#

public class ResponseHeaderAttribute : ActionFilterAttribute


{
private readonly string _name;
private readonly string _value;

public ResponseHeaderAttribute(string name, string value) =>


(_name, _value) = (name, value);

public override void OnResultExecuting(ResultExecutingContext context)


{
context.HttpContext.Response.Headers.Add(_name, _value);

base.OnResultExecuting(context);
}
}

Attributes allow filters to accept arguments, as shown in the preceding example. Apply
the ResponseHeaderAttribute to a controller or action method and specify the name and
value of the HTTP header:

C#

[ResponseHeader("Filter-Header", "Filter Value")]


public class ResponseHeaderController : ControllerBase
{
public IActionResult Index() =>
Content("Examine the response headers using the F12 developer
tools.");

// ...

Use a tool such as the browser developer tools to examine the headers. Under
Response Headers, filter-header: Filter Value is displayed.

The following code applies ResponseHeaderAttribute to both a controller and an action:

C#

[ResponseHeader("Filter-Header", "Filter Value")]


public class ResponseHeaderController : ControllerBase
{
public IActionResult Index() =>
Content("Examine the response headers using the F12 developer
tools.");

// ...

[ResponseHeader("Another-Filter-Header", "Another Filter Value")]


public IActionResult Multiple() =>
Content("Examine the response headers using the F12 developer
tools.");
}

Responses from the Multiple action include the following headers:

filter-header: Filter Value

another-filter-header: Another Filter Value

Several of the filter interfaces have corresponding attributes that can be used as base
classes for custom implementations.

Filter attributes:

ActionFilterAttribute
ExceptionFilterAttribute
ResultFilterAttribute
FormatFilterAttribute
ServiceFilterAttribute
TypeFilterAttribute

Filters cannot be applied to Razor Page handler methods. They can be applied either to
the Razor Page model or globally.
Filter scopes and order of execution
A filter can be added to the pipeline at one of three scopes:

Using an attribute on a controller or Razor Page.


Using an attribute on a controller action. Filter attributes cannot be applied to
Razor Pages handler methods.
Globally for all controllers, actions, and Razor Pages as shown in the following
code:

C#

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.


builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add<GlobalSampleActionFilter>();
});

Default order of execution


When there are multiple filters for a particular stage of the pipeline, scope determines
the default order of filter execution. Global filters surround class filters, which in turn
surround method filters.

As a result of filter nesting, the after code of filters runs in the reverse order of the before
code. The filter sequence:

The before code of global filters.


The before code of controller filters.
The before code of action method filters.
The after code of action method filters.
The after code of controller filters.
The after code of global filters.

The following example illustrates the order in which filter methods run for synchronous
action filters:

Sequence Filter scope Filter method

1 Global OnActionExecuting

2 Controller OnActionExecuting
Sequence Filter scope Filter method

3 Action OnActionExecuting

4 Action OnActionExecuted

5 Controller OnActionExecuted

6 Global OnActionExecuted

Controller level filters


Every controller that inherits from Controller includes the OnActionExecuting,
OnActionExecutionAsync, and OnActionExecuted methods. These methods wrap the
filters that run for a given action:

OnActionExecuting runs before any of the action's filters.

OnActionExecuted runs after all of the action's filters.


OnActionExecutionAsync runs before any of the action's filters. Code after a call to

next runs after the action's filters.

The following ControllerFiltersController class:

Applies the SampleActionFilterAttribute ( [SampleActionFilter] ) to the controller.


Overrides OnActionExecuting and OnActionExecuted .

C#

[SampleActionFilter]
public class ControllerFiltersController : Controller
{
public override void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine(
$"- {nameof(ControllerFiltersController)}.
{nameof(OnActionExecuting)}");

base.OnActionExecuting(context);
}

public override void OnActionExecuted(ActionExecutedContext context)


{
Console.WriteLine(
$"- {nameof(ControllerFiltersController)}.
{nameof(OnActionExecuted)}");

base.OnActionExecuted(context);
}
public IActionResult Index()
{
Console.WriteLine(
$"- {nameof(ControllerFiltersController)}.{nameof(Index)}");

return Content("Check the Console.");


}
}

Navigating to https://localhost:<port>/ControllerFilters runs the following code:

ControllerFiltersController.OnActionExecuting

GlobalSampleActionFilter.OnActionExecuting

SampleActionFilterAttribute.OnActionExecuting
ControllerFiltersController.Index

SampleActionFilterAttribute.OnActionExecuted
GlobalSampleActionFilter.OnActionExecuted

ControllerFiltersController.OnActionExecuted

Controller level filters set the Order property to int.MinValue . Controller level filters
can not be set to run after filters applied to methods. Order is explained in the next
section.

For Razor Pages, see Implement Razor Page filters by overriding filter methods.

Override the default order


The default sequence of execution can be overridden by implementing IOrderedFilter.
IOrderedFilter exposes the Order property that takes precedence over scope to

determine the order of execution. A filter with a lower Order value:

Runs the before code before that of a filter with a higher value of Order .
Runs the after code after that of a filter with a higher Order value.

In the Controller level filters example, GlobalSampleActionFilter has global scope so it


runs before SampleActionFilterAttribute , which has controller scope. To make
SampleActionFilterAttribute run first, set its order to int.MinValue :

C#

[SampleActionFilter(Order = int.MinValue)]
public class ControllerFiltersController : Controller
{
// ...
}

To make the global filter GlobalSampleActionFilter run first, set its Order to
int.MinValue :

C#

builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add<GlobalSampleActionFilter>(int.MinValue);
});

Cancellation and short-circuiting


The filter pipeline can be short-circuited by setting the Result property on the
ResourceExecutingContext parameter provided to the filter method. For example, the
following Resource filter prevents the rest of the pipeline from executing:

C#

public class ShortCircuitingResourceFilterAttribute : Attribute,


IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
context.Result = new ContentResult
{
Content = nameof(ShortCircuitingResourceFilterAttribute)
};
}

public void OnResourceExecuted(ResourceExecutedContext context) { }


}

In the following code, both the [ShortCircuitingResourceFilter] and the


[ResponseHeader] filter target the Index action method. The
ShortCircuitingResourceFilterAttribute filter:

Runs first, because it's a Resource Filter and ResponseHeaderAttribute is an Action


Filter.
Short-circuits the rest of the pipeline.

Therefore the ResponseHeaderAttribute filter never runs for the Index action. This
behavior would be the same if both filters were applied at the action method level,
provided the ShortCircuitingResourceFilterAttribute ran first. The
ShortCircuitingResourceFilterAttribute runs first because of its filter type:

C#

[ResponseHeader("Filter-Header", "Filter Value")]


public class ShortCircuitingController : Controller
{
[ShortCircuitingResourceFilter]
public IActionResult Index() =>
Content($"- {nameof(ShortCircuitingController)}.{nameof(Index)}");
}

Dependency injection
Filters can be added by type or by instance. If an instance is added, that instance is used
for every request. If a type is added, it's type-activated. A type-activated filter means:

An instance is created for each request.


Any constructor dependencies are populated by dependency injection (DI).

Filters that are implemented as attributes and added directly to controller classes or
action methods cannot have constructor dependencies provided by dependency
injection (DI). Constructor dependencies cannot be provided by DI because attributes
must have their constructor parameters supplied where they're applied.

The following filters support constructor dependencies provided from DI:

ServiceFilterAttribute
TypeFilterAttribute
IFilterFactory implemented on the attribute.

The preceding filters can be applied to a controller or an action.

Loggers are available from DI. However, avoid creating and using filters purely for
logging purposes. The built-in framework logging typically provides what's needed for
logging. Logging added to filters:

Should focus on business domain concerns or behavior specific to the filter.


Should not log actions or other framework events. The built-in filters already log
actions and framework events.

ServiceFilterAttribute
Service filter implementation types are registered in Program.cs . A ServiceFilterAttribute
retrieves an instance of the filter from DI.

The following code shows the LoggingResponseHeaderFilterService class, which uses DI:

C#

public class LoggingResponseHeaderFilterService : IResultFilter


{
private readonly ILogger _logger;

public LoggingResponseHeaderFilterService(
ILogger<LoggingResponseHeaderFilterService> logger) =>
_logger = logger;

public void OnResultExecuting(ResultExecutingContext context)


{
_logger.LogInformation(
$"- {nameof(LoggingResponseHeaderFilterService)}.
{nameof(OnResultExecuting)}");

context.HttpContext.Response.Headers.Add(
nameof(OnResultExecuting),
nameof(LoggingResponseHeaderFilterService));
}

public void OnResultExecuted(ResultExecutedContext context)


{
_logger.LogInformation(
$"- {nameof(LoggingResponseHeaderFilterService)}.
{nameof(OnResultExecuted)}");
}
}

In the following code, LoggingResponseHeaderFilterService is added to the DI container:

C#

builder.Services.AddScoped<LoggingResponseHeaderFilterService>();

In the following code, the ServiceFilter attribute retrieves an instance of the


LoggingResponseHeaderFilterService filter from DI:

C#

[ServiceFilter(typeof(LoggingResponseHeaderFilterService))]
public IActionResult WithServiceFilter() =>
Content($"- {nameof(FilterDependenciesController)}.
{nameof(WithServiceFilter)}");
When using ServiceFilterAttribute , setting ServiceFilterAttribute.IsReusable:

Provides a hint that the filter instance may be reused outside of the request scope
it was created within. The ASP.NET Core runtime doesn't guarantee:
That a single instance of the filter will be created.
The filter will not be re-requested from the DI container at some later point.
Shouldn't be used with a filter that depends on services with a lifetime other than
singleton.

ServiceFilterAttribute implements IFilterFactory. IFilterFactory exposes the


CreateInstance method for creating an IFilterMetadata instance. CreateInstance loads
the specified type from DI.

TypeFilterAttribute
TypeFilterAttribute is similar to ServiceFilterAttribute, but its type isn't resolved directly
from the DI container. It instantiates the type by using
Microsoft.Extensions.DependencyInjection.ObjectFactory.

Because TypeFilterAttribute types aren't resolved directly from the DI container:

Types that are referenced using the TypeFilterAttribute don't need to be


registered with the DI container. They do have their dependencies fulfilled by the
DI container.
TypeFilterAttribute can optionally accept constructor arguments for the type.

When using TypeFilterAttribute , setting TypeFilterAttribute.IsReusable:

Provides hint that the filter instance may be reused outside of the request scope it
was created within. The ASP.NET Core runtime provides no guarantees that a
single instance of the filter will be created.

Should not be used with a filter that depends on services with a lifetime other than
singleton.

The following example shows how to pass arguments to a type using


TypeFilterAttribute :

C#

[TypeFilter(typeof(LoggingResponseHeaderFilter),
Arguments = new object[] { "Filter-Header", "Filter Value" })]
public IActionResult WithTypeFilter() =>
Content($"- {nameof(FilterDependenciesController)}.
{nameof(WithTypeFilter)}");

Authorization filters
Authorization filters:

Are the first filters run in the filter pipeline.


Control access to action methods.
Have a before method, but no after method.

Custom authorization filters require a custom authorization framework. Prefer


configuring the authorization policies or writing a custom authorization policy over
writing a custom filter. The built-in authorization filter:

Calls the authorization system.


Does not authorize requests.

Do not throw exceptions within authorization filters:

The exception will not be handled.


Exception filters will not handle the exception.

Consider issuing a challenge when an exception occurs in an authorization filter.

Learn more about Authorization.

Resource filters
Resource filters:

Implement either the IResourceFilter or IAsyncResourceFilter interface.


Execution wraps most of the filter pipeline.
Only Authorization filters run before resource filters.

Resource filters are useful to short-circuit most of the pipeline. For example, a caching
filter can avoid the rest of the pipeline on a cache hit.

Resource filter examples:

The short-circuiting resource filter shown previously.

DisableFormValueModelBindingAttribute :
Prevents model binding from accessing the form data.
Used for large file uploads to prevent the form data from being read into
memory.

Action filters
Action filters do not apply to Razor Pages. Razor Pages supports IPageFilter and
IAsyncPageFilter. For more information, see Filter methods for Razor Pages.

Action filters:

Implement either the IActionFilter or IAsyncActionFilter interface.


Their execution surrounds the execution of action methods.

The following code shows a sample action filter:

C#

public class SampleActionFilter : IActionFilter


{
public void OnActionExecuting(ActionExecutingContext context)
{
// Do something before the action executes.
}

public void OnActionExecuted(ActionExecutedContext context)


{
// Do something after the action executes.
}
}

The ActionExecutingContext provides the following properties:

ActionArguments - enables reading the inputs to an action method.


Controller - enables manipulating the controller instance.
Result - setting Result short-circuits execution of the action method and
subsequent action filters.

Throwing an exception in an action method:

Prevents running of subsequent filters.


Unlike setting Result , is treated as a failure instead of a successful result.

The ActionExecutedContext provides Controller and Result plus the following


properties:

Canceled - True if the action execution was short-circuited by another filter.


Exception - Non-null if the action or a previously run action filter threw an
exception. Setting this property to null:
Effectively handles the exception.
Result is executed as if it was returned from the action method.

For an IAsyncActionFilter , a call to the ActionExecutionDelegate:

Executes any subsequent action filters and the action method.


Returns ActionExecutedContext .

To short-circuit, assign Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext.Result


to a result instance and don't call next (the ActionExecutionDelegate ).

The framework provides an abstract ActionFilterAttribute that can be subclassed.

The OnActionExecuting action filter can be used to:

Validate model state.


Return an error if the state is invalid.

C#

public class ValidateModelAttribute : ActionFilterAttribute


{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}

7 Note

Controllers annotated with the [ApiController] attribute automatically validate


model state and return a 400 response. For more information, see Automatic HTTP
400 responses.

The OnActionExecuted method runs after the action method:

And can see and manipulate the results of the action through the Result property.
Canceled is set to true if the action execution was short-circuited by another filter.
Exception is set to a non-null value if the action or a subsequent action filter threw
an exception. Setting Exception to null:
Effectively handles an exception.
ActionExecutedContext.Result is executed as if it were returned normally from
the action method.

Exception filters
Exception filters:

Implement IExceptionFilter or IAsyncExceptionFilter.


Can be used to implement common error handling policies.

The following sample exception filter displays details about exceptions that occur when
the app is in development:

C#

public class SampleExceptionFilter : IExceptionFilter


{
private readonly IHostEnvironment _hostEnvironment;

public SampleExceptionFilter(IHostEnvironment hostEnvironment) =>


_hostEnvironment = hostEnvironment;

public void OnException(ExceptionContext context)


{
if (!_hostEnvironment.IsDevelopment())
{
// Don't display exception details unless running in
Development.
return;
}

context.Result = new ContentResult


{
Content = context.Exception.ToString()
};
}
}

The following code tests the exception filter:

C#

[TypeFilter(typeof(SampleExceptionFilter))]
public class ExceptionController : Controller
{
public IActionResult Index() =>
Content($"- {nameof(ExceptionController)}.{nameof(Index)}");
}
Exception filters:

Don't have before and after events.


Implement OnException or OnExceptionAsync.
Handle unhandled exceptions that occur in Razor Page or controller creation,
model binding, action filters, or action methods.
Do not catch exceptions that occur in resource filters, result filters, or MVC result
execution.

To handle an exception, set the ExceptionHandled property to true or assign the Result
property. This stops propagation of the exception. An exception filter can't turn an
exception into a "success". Only an action filter can do that.

Exception filters:

Are good for trapping exceptions that occur within actions.


Are not as flexible as error handling middleware.

Prefer middleware for exception handling. Use exception filters only where error
handling differs based on which action method is called. For example, an app might
have action methods for both API endpoints and for views/HTML. The API endpoints
could return error information as JSON, while the view-based actions could return an
error page as HTML.

Result filters
Result filters:

Implement an interface:
IResultFilter or IAsyncResultFilter
IAlwaysRunResultFilter or IAsyncAlwaysRunResultFilter
Their execution surrounds the execution of action results.

IResultFilter and IAsyncResultFilter


The following code shows a sample result filter:

C#

public class SampleResultFilter : IResultFilter


{
public void OnResultExecuting(ResultExecutingContext context)
{
// Do something before the result executes.
}

public void OnResultExecuted(ResultExecutedContext context)


{
// Do something after the result executes.
}
}

The kind of result being executed depends on the action. An action returning a view
includes all razor processing as part of the ViewResult being executed. An API method
might perform some serialization as part of the execution of the result. Learn more
about action results.

Result filters are only executed when an action or action filter produces an action result.
Result filters are not executed when:

An authorization filter or resource filter short-circuits the pipeline.


An exception filter handles an exception by producing an action result.

The Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuting method can short-


circuit execution of the action result and subsequent result filters by setting
Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext.Cancel to true . Write to the
response object when short-circuiting to avoid generating an empty response. Throwing
an exception in IResultFilter.OnResultExecuting :

Prevents execution of the action result and subsequent filters.


Is treated as a failure instead of a successful result.

When the Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuted method runs,


the response has probably already been sent to the client. If the response has already
been sent to the client, it cannot be changed.

ResultExecutedContext.Canceled is set to true if the action result execution was short-


circuited by another filter.

ResultExecutedContext.Exception is set to a non-null value if the action result or a


subsequent result filter threw an exception. Setting Exception to null effectively handles
an exception and prevents the exception from being thrown again later in the pipeline.
There is no reliable way to write data to a response when handling an exception in a
result filter. If the headers have been flushed to the client when an action result throws
an exception, there's no reliable mechanism to send a failure code.

For an IAsyncResultFilter, a call to await next on the ResultExecutionDelegate executes


any subsequent result filters and the action result. To short-circuit, set
ResultExecutingContext.Cancel to true and don't call the ResultExecutionDelegate :

C#

public class SampleAsyncResultFilter : IAsyncResultFilter


{
public async Task OnResultExecutionAsync(
ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.Result is not EmptyResult)
{
await next();
}
else
{
context.Cancel = true;
}
}
}

The framework provides an abstract ResultFilterAttribute that can be subclassed. The


ResponseHeaderAttribute class shown previously is an example of a result filter
attribute.

IAlwaysRunResultFilter and IAsyncAlwaysRunResultFilter


The IAlwaysRunResultFilter and IAsyncAlwaysRunResultFilter interfaces declare an
IResultFilter implementation that runs for all action results. This includes action results
produced by:

Authorization filters and resource filters that short-circuit.


Exception filters.

For example, the following filter always runs and sets an action result (ObjectResult) with
a 422 Unprocessable Entity status code when content negotiation fails:

C#

public class UnprocessableResultFilter : IAlwaysRunResultFilter


{
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is StatusCodeResult statusCodeResult
&& statusCodeResult.StatusCode ==
StatusCodes.Status415UnsupportedMediaType)
{
context.Result = new ObjectResult("Unprocessable")
{
StatusCode = StatusCodes.Status422UnprocessableEntity
};
}
}

public void OnResultExecuted(ResultExecutedContext context) { }


}

IFilterFactory
IFilterFactory implements IFilterMetadata. Therefore, an IFilterFactory instance can be
used as an IFilterMetadata instance anywhere in the filter pipeline. When the runtime
prepares to invoke the filter, it attempts to cast it to an IFilterFactory . If that cast
succeeds, the CreateInstance method is called to create the IFilterMetadata instance
that is invoked. This provides a flexible design, since the precise filter pipeline doesn't
need to be set explicitly when the app starts.

IFilterFactory.IsReusable :

Is a hint by the factory that the filter instance created by the factory may be reused
outside of the request scope it was created within.
Should not be used with a filter that depends on services with a lifetime other than
singleton.

The ASP.NET Core runtime doesn't guarantee:

That a single instance of the filter will be created.


The filter will not be re-requested from the DI container at some later point.

2 Warning

Only configure IFilterFactory.IsReusable to return true if the source of the filters is


unambiguous, the filters are stateless, and the filters are safe to use across multiple
HTTP requests. For instance, don't return filters from DI that are registered as
scoped or transient if IFilterFactory.IsReusable returns true .

IFilterFactory can be implemented using custom attribute implementations as


another approach to creating filters:

C#

public class ResponseHeaderFilterFactory : Attribute, IFilterFactory


{
public bool IsReusable => false;

public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)


=>
new InternalResponseHeaderFilter();

private class InternalResponseHeaderFilter : IActionFilter


{
public void OnActionExecuting(ActionExecutingContext context) =>
context.HttpContext.Response.Headers.Add(
nameof(OnActionExecuting),
nameof(InternalResponseHeaderFilter));

public void OnActionExecuted(ActionExecutedContext context) { }


}

The filter is applied in the following code:

C#

[ResponseHeaderFilterFactory]
public IActionResult Index() =>
Content($"- {nameof(FilterFactoryController)}.{nameof(Index)}");

IFilterFactory implemented on an attribute


Filters that implement IFilterFactory are useful for filters that:

Don't require passing parameters.


Have constructor dependencies that need to be filled by DI.

TypeFilterAttribute implements IFilterFactory. IFilterFactory exposes the


CreateInstance method for creating an IFilterMetadata instance. CreateInstance loads
the specified type from the services container (DI).

C#

public class SampleActionTypeFilterAttribute : TypeFilterAttribute


{
public SampleActionTypeFilterAttribute()
: base(typeof(InternalSampleActionFilter)) { }

private class InternalSampleActionFilter : IActionFilter


{
private readonly ILogger<InternalSampleActionFilter> _logger;

public
InternalSampleActionFilter(ILogger<InternalSampleActionFilter> logger) =>
_logger = logger;
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation(
$"- {nameof(InternalSampleActionFilter)}.
{nameof(OnActionExecuting)}");
}

public void OnActionExecuted(ActionExecutedContext context)


{
_logger.LogInformation(
$"- {nameof(InternalSampleActionFilter)}.
{nameof(OnActionExecuted)}");
}
}
}

The following code shows three approaches to applying the filter:

C#

[SampleActionTypeFilter]
public IActionResult WithDirectAttribute() =>
Content($"- {nameof(FilterFactoryController)}.
{nameof(WithDirectAttribute)}");

[TypeFilter(typeof(SampleActionTypeFilterAttribute))]
public IActionResult WithTypeFilterAttribute() =>
Content($"- {nameof(FilterFactoryController)}.
{nameof(WithTypeFilterAttribute)}");

[ServiceFilter(typeof(SampleActionTypeFilterAttribute))]
public IActionResult WithServiceFilterAttribute() =>
Content($"- {nameof(FilterFactoryController)}.
{nameof(WithServiceFilterAttribute)}");

In the preceding code, the first approach to applying the filter is preferred.

Use middleware in the filter pipeline


Resource filters work like middleware in that they surround the execution of everything
that comes later in the pipeline. But filters differ from middleware in that they're part of
the runtime, which means that they have access to context and constructs.

To use middleware as a filter, create a type with a Configure method that specifies the
middleware to inject into the filter pipeline. The following example uses middleware to
set a response header:
C#

public class FilterMiddlewarePipeline


{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
context.Response.Headers.Add("Pipeline", "Middleware");

await next();
});
}
}

Use the MiddlewareFilterAttribute to run the middleware:

C#

[MiddlewareFilter(typeof(FilterMiddlewarePipeline))]
public class FilterMiddlewareController : Controller
{
public IActionResult Index() =>
Content($"- {nameof(FilterMiddlewareController)}.{nameof(Index)}");
}

Middleware filters run at the same stage of the filter pipeline as Resource filters, before
model binding and after the rest of the pipeline.

Thread safety
When passing an instance of a filter into Add , instead of its Type , the filter is a singleton
and is not thread-safe.

Additional resources
View or download sample (how to download).
Filter methods for Razor Pages in ASP.NET Core
ASP.NET Core Razor SDK
Article • 03/18/2023

By Rick Anderson

Overview
The .NET 6.0 SDK includes the Microsoft.NET.Sdk.Razor MSBuild SDK (Razor SDK).
The Razor SDK:

Is required to build, package, and publish projects containing Razor files for
ASP.NET Core MVC-based or Blazor projects.
Includes a set of predefined properties, and items that allow customizing the
compilation of Razor ( .cshtml or .razor ) files.

The Razor SDK includes Content items with Include attributes set to the **\*.cshtml
and **\*.razor globbing patterns. Matching files are published.

Prerequisites
.NET 6.0 SDK

Use the Razor SDK


Most web apps aren't required to explicitly reference the Razor SDK.

To use the Razor SDK to build class libraries containing Razor views or Razor Pages, we
recommend starting with the Razor class library (RCL) project template. An RCL that's
used to build Blazor ( .razor ) files minimally requires a reference to the
Microsoft.AspNetCore.Components package. An RCL that's used to build Razor views
or pages ( .cshtml files) minimally requires targeting netcoreapp3.0 or later and has a
FrameworkReference to the Microsoft.AspNetCore.App metapackage in its project file.

Properties
The following properties control the Razor's SDK behavior as part of a project build:

RazorCompileOnBuild : When true , compiles and emits the Razor assembly as part
of building the project. Defaults to true .
RazorCompileOnPublish : When true , compiles and emits the Razor assembly as

part of publishing the project. Defaults to true .


UseRazorSourceGenerator : Defaults to true . When true :

Compiles using source generation.


Doesn't create <app_name>.Views.dll . Views are included in <app_name>.dll .
Supports .NET Hot Reload.

The properties and items in the following table are used to configure inputs and output
to the Razor SDK.

Items Description

RazorGenerate Item elements ( .cshtml files) that are inputs to code generation.

RazorComponent Item elements ( .razor files) that are inputs to Razor component code
generation.

RazorCompile Item elements ( .cs files) that are inputs to Razor compilation targets.
Use this ItemGroup to specify additional files to be compiled into the
Razor assembly.

RazorEmbeddedResource Item elements added as embedded resources to the generated Razor


assembly.

Property Description

RazorOutputPath The Razor output directory.

RazorCompileToolset Used to determine the toolset used to build


the Razor assembly. Valid values are
Implicit , RazorSDK , and PrecompilationTool .

EnableDefaultContentItems Default is true . When true , includes


web.config, .json , and .cshtml files as
content in the project. When referenced via
Microsoft.NET.Sdk.Web , files under wwwroot
and config files are also included.

EnableDefaultRazorGenerateItems When true , includes .cshtml files from


Content items in RazorGenerate items.

GenerateRazorTargetAssemblyInfo Not used in .NET 6 and later.

EnableDefaultRazorTargetAssemblyInfoAttributes Not used in .NET 6 and later.


Property Description

CopyRazorGenerateFilesToPublishDirectory When true , copies RazorGenerate items


( .cshtml ) files to the publish directory.
Typically, Razor files aren't required for a
published app if they participate in
compilation at build-time or publish-time.
Defaults to false .

PreserveCompilationReferences When true , copy reference assembly items to


the publish directory. Typically, reference
assemblies aren't required for a published
app if Razor compilation occurs at build-time
or publish-time. Set to true if your published
app requires runtime compilation. For
example, set the value to true if the app
modifies .cshtml files at runtime or uses
embedded views. Defaults to false .

IncludeRazorContentInPack When true , all Razor content items ( .cshtml


files) are marked for inclusion in the
generated NuGet package. Defaults to false .

EmbedRazorGenerateSources When true , adds RazorGenerate ( .cshtml )


items as embedded files to the generated
Razor assembly. Defaults to false .

GenerateMvcApplicationPartsAssemblyAttributes Not used in .NET 6 and later.

DefaultWebContentItemExcludes A globbing pattern for item elements that are


to be excluded from the Content item group
in projects targeting the Web or Razor SDK

ExcludeConfigFilesFromBuildOutput When true , .config and .json files do not get


copied to the build output directory.

AddRazorSupportForMvc When true , configures the Razor SDK to add


support for the MVC configuration that is
required when building applications
containing MVC views or Razor Pages. This
property is implicitly set for .NET Core 3.0 or
later projects targeting the Web SDK

RazorLangVersion The version of the Razor Language to target.

EmitCompilerGeneratedFiles When set to true , the generated source files


are written to disk. Setting to true is useful
when debugging the compiler. The default is
false .
For more information on properties, see MSBuild properties.

Runtime compilation of Razor views


By default, the Razor SDK doesn't publish reference assemblies that are required to
perform runtime compilation. This results in compilation failures when the
application model relies on runtime compilation—for example, the app uses
embedded views or changes views after the app is published. Set
CopyRefAssembliesToPublishDirectory to true to continue publishing reference
assemblies. Both code generation and compilation are supported by a single call
to the compiler. A single assembly is produced that contains the app types and the
generated views.

For a web app, ensure your app is targeting the Microsoft.NET.Sdk.Web SDK.

Razor language version


When targeting the Microsoft.NET.Sdk.Web SDK, the Razor language version is inferred
from the app's target framework version. For projects targeting the
Microsoft.NET.Sdk.Razor SDK or in the rare case that the app requires a different Razor

language version than the inferred value, a version can be configured by setting the
<RazorLangVersion> property in the app's project file:

XML

<PropertyGroup>
<RazorLangVersion>{VERSION}</RazorLangVersion>
</PropertyGroup>

Razor's language version is tightly integrated with the version of the runtime that it was
built for. Targeting a language version that isn't designed for the runtime is unsupported
and likely produces build errors.

Additional resources
Additions to the csproj format for .NET Core
Common MSBuild project items
View components in ASP.NET Core
Article • 06/03/2022

By Rick Anderson

View components
View components are similar to partial views, but they're much more powerful. View
components don't use model binding, they depend on the data passed when calling the
view component. This article was written using controllers and views, but view
components work with Razor Pages .

A view component:

Renders a chunk rather than a whole response.


Includes the same separation-of-concerns and testability benefits found between a
controller and view.
Can have parameters and business logic.
Is typically invoked from a layout page.

View components are intended anywhere reusable rendering logic that's too complex
for a partial view, such as:

Dynamic navigation menus


Tag cloud, where it queries the database
Sign in panel
Shopping cart
Recently published articles
Sidebar content on a blog
A sign in panel that would be rendered on every page and show either the links to
sign out or sign in, depending on the sign in state of the user

A view component consists of two parts:

The class, typically derived from ViewComponent


The result it returns, typically a view.

Like controllers, a view component can be a POCO, but most developers take advantage
of the methods and properties available by deriving from ViewComponent.

When considering if view components meet an app's specifications, consider using


Razor components instead. Razor components also combine markup with C# code to
produce reusable UI units. Razor components are designed for developer productivity
when providing client-side UI logic and composition. For more information, see ASP.NET
Core Razor components. For information on how to incorporate Razor components into
an MVC or Razor Pages app, see Prerender and integrate ASP.NET Core Razor
components.

Create a view component


This section contains the high-level requirements to create a view component. Later in
the article, we'll examine each step in detail and create a view component.

The view component class


A view component class can be created by any of the following:

Deriving from ViewComponent


Decorating a class with the [ViewComponent] attribute, or deriving from a class
with the [ViewComponent] attribute
Creating a class where the name ends with the suffix ViewComponent

Like controllers, view components must be public, non-nested, and non-abstract classes.
The view component name is the class name with the ViewComponent suffix removed. It
can also be explicitly specified using the Name property.

A view component class:

Supports constructor dependency injection


Doesn't take part in the controller lifecycle, therefore filters can't be used in a view
component

To prevent a class that has a case-insensitive ViewComponent suffix from being treated as
a view component, decorate the class with the [NonViewComponent] attribute:

C#

using Microsoft.AspNetCore.Mvc;

[NonViewComponent]
public class ReviewComponent
{
public string Status(string name) => JobStatus.GetCurrentStatus(name);
}
View component methods
A view component defines its logic in an:

InvokeAsync method that returns Task<IViewComponentResult> .

Invoke synchronous method that returns an IViewComponentResult.

Parameters come directly from invocation of the view component, not from model
binding. A view component never directly handles a request. Typically, a view
component initializes a model and passes it to a view by calling the View method. In
summary, view component methods:

Define an InvokeAsync method that returns a Task<IViewComponentResult> or a


synchronous Invoke method that returns an IViewComponentResult .
Typically initializes a model and passes it to a view by calling the
ViewComponent.View method.
Parameters come from the calling method, not HTTP. There's no model binding.
Aren't reachable directly as an HTTP endpoint. They're typically invoked in a view.
A view component never handles a request.
Are overloaded on the signature rather than any details from the current HTTP
request.

View search path


The runtime searches for the view in the following paths:

/Views/{Controller Name}/Components/{View Component Name}/{View Name}


/Views/Shared/Components/{View Component Name}/{View Name}
/Pages/Shared/Components/{View Component Name}/{View Name}

The search path applies to projects using controllers + views and Razor Pages.

The default view name for a view component is Default , which means view files will
typically be named Default.cshtml . A different view name can be specified when
creating the view component result or when calling the View method.

We recommend naming the view file Default.cshtml and using the


Views/Shared/Components/{View Component Name}/{View Name} path. The
PriorityList view component used in this sample uses

Views/Shared/Components/PriorityList/Default.cshtml for the view component view.

Customize the view search path


To customize the view search path, modify Razor's ViewLocationFormats collection. For
example, to search for views within the path /Components/{View Component Name}/{View
Name} , add a new item to the collection:

C#

using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews()
.AddRazorOptions(options =>
{
options.ViewLocationFormats.Add("/{0}.cshtml");
});

builder.Services.AddDbContext<ToDoContext>(options =>
options.UseInMemoryDatabase("db"));

var app = builder.Build();

// Remaining code removed for brevity.

In the preceding code, the placeholder {0} represents the path Components/{View
Component Name}/{View Name} .

Invoke a view component


To use the view component, call the following inside a view:

CSHTML

@await Component.InvokeAsync("Name of view component",


{Anonymous Type Containing Parameters})

The parameters are passed to the InvokeAsync method. The PriorityList view
component developed in the article is invoked from the Views/ToDo/Index.cshtml view
file. In the following code, the InvokeAsync method is called with two parameters:

CSHTML

</table>

<div>
Maxium Priority: @ViewData["maxPriority"] <br />
Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync("PriorityList",
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>

Invoke a view component as a Tag Helper


A View Component can be invoked as a Tag Helper:

CSHTML

<div>
Maxium Priority: @ViewData["maxPriority"] <br />
Is Complete: @ViewData["isDone"]
@{
int maxPriority = Convert.ToInt32(ViewData["maxPriority"]);
bool isDone = Convert.ToBoolean(ViewData["isDone"]);
}
<vc:priority-list max-priority=maxPriority is-done=isDone>
</vc:priority-list>
</div>

Pascal-cased class and method parameters for Tag Helpers are translated into their
kebab case . The Tag Helper to invoke a view component uses the <vc></vc> element.
The view component is specified as follows:

CSHTML

<vc:[view-component-name]
parameter1="parameter1 value"
parameter2="parameter2 value">
</vc:[view-component-name]>

To use a view component as a Tag Helper, register the assembly containing the view
component using the @addTagHelper directive. If the view component is in an assembly
called MyWebApp , add the following directive to the _ViewImports.cshtml file:

CSHTML

@addTagHelper *, MyWebApp

A view component can be registered as a Tag Helper to any file that references the view
component. See Managing Tag Helper Scope for more information on how to register
Tag Helpers.

The InvokeAsync method used in this tutorial:

CSHTML

@await Component.InvokeAsync("PriorityList",
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)

In the preceding markup, the PriorityList view component becomes priority-list .


The parameters to the view component are passed as attributes in kebab case.

Invoke a view component directly from a controller


View components are typically invoked from a view, but they can be invoked directly
from a controller method. While view components don't define endpoints like
controllers, a controller action that returns the content of a ViewComponentResult can be
implemented.

In the following example, the view component is called directly from the controller:

C#

public IActionResult IndexVC(int maxPriority = 2, bool isDone = false)


{
return ViewComponent("PriorityList",
new {
maxPriority = maxPriority,
isDone = isDone
});
}

Create a basic view component


Download , build and test the starter code. It's a basic project with a ToDo controller
that displays a list of ToDo items.
Update the controller to pass in priority and completion
status
Update the Index method to use priority and completion status parameters:

C#

using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;

namespace ViewComponentSample.Controllers;
public class ToDoController : Controller
{
private readonly ToDoContext _ToDoContext;

public ToDoController(ToDoContext context)


{
_ToDoContext = context;
_ToDoContext.Database.EnsureCreated();
}

public IActionResult Index(int maxPriority = 2, bool isDone = false)


{
var model = _ToDoContext!.ToDo!.ToList();
ViewData["maxPriority"] = maxPriority;
ViewData["isDone"] = isDone;
return View(model);
}
Add a ViewComponent class
Add a ViewComponent class to ViewComponents/PriorityListViewComponent.cs :

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents;

public class PriorityListViewComponent : ViewComponent


{
private readonly ToDoContext db;

public PriorityListViewComponent(ToDoContext context) => db = context;

public async Task<IViewComponentResult> InvokeAsync(


int maxPriority, bool isDone)
{
var items = await GetItemsAsync(maxPriority, isDone);
return View(items);
}

private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)


{
return db!.ToDo!.Where(x => x.IsDone == isDone &&
x.Priority <= maxPriority).ToListAsync();
}
}

Notes on the code:

View component classes can be contained in any folder in the project.

Because the class name PriorityListViewComponent ends with the suffix


ViewComponent, the runtime uses the string PriorityList when referencing the
class component from a view.

The [ViewComponent] attribute can change the name used to reference a view
component. For example, the class could have been named XYZ with the following
[ViewComponent] attribute:

C#

[ViewComponent(Name = "PriorityList")]
public class XYZ : ViewComponent
The [ViewComponent] attribute in the preceding code tells the view component
selector to use:
The name PriorityList when looking for the views associated with the
component
The string "PriorityList" when referencing the class component from a view.

The component uses dependency injection to make the data context available.

InvokeAsync exposes a method that can be called from a view, and it can take an
arbitrary number of arguments.

The InvokeAsync method returns the set of ToDo items that satisfy the isDone and
maxPriority parameters.

Create the view component Razor view


Create the Views/Shared/Components folder. This folder must be named
Components.

Create the Views/Shared/Components/PriorityList folder. This folder name must


match the name of the view component class, or the name of the class minus the
suffix. If the ViewComponent attribute is used, the class name would need to match
the attribute designation.

Create a Views/Shared/Components/PriorityList/Default.cshtml Razor view:

CSHTML

@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h3>Priority Items</h3>
<ul>
@foreach (var todo in Model)
{
<li>@todo.Name</li>
}
</ul>

The Razor view takes a list of TodoItem and displays them. If the view component
InvokeAsync method doesn't pass the name of the view, Default is used for the

view name by convention. To override the default styling for a specific controller,
add a view to the controller-specific view folder (for example
Views/ToDo/Components/PriorityList/Default.cshtml).
If the view component is controller-specific, it can be added to the controller-
specific folder. For example, Views/ToDo/Components/PriorityList/Default.cshtml
is controller-specific.

Add a div containing a call to the priority list component to the bottom of the
Views/ToDo/index.cshtml file:

CSHTML

</table>

<div>
Maxium Priority: @ViewData["maxPriority"] <br />
Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync("PriorityList",
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>

The markup @await Component.InvokeAsync shows the syntax for calling view
components. The first argument is the name of the component we want to invoke or
call. Subsequent parameters are passed to the component. InvokeAsync can take an
arbitrary number of arguments.

Test the app. The following image shows the ToDo list and the priority items:
The view component can be called directly from the controller:

C#

public IActionResult IndexVC(int maxPriority = 2, bool isDone = false)


{
return ViewComponent("PriorityList",
new {
maxPriority = maxPriority,
isDone = isDone
});
}
Specify a view component name
A complex view component might need to specify a non-default view under some
conditions. The following code shows how to specify the "PVC" view from the
InvokeAsync method. Update the InvokeAsync method in the
PriorityListViewComponent class.

C#

public async Task<IViewComponentResult> InvokeAsync(


int maxPriority, bool isDone)
{
string MyView = "Default";
// If asking for all completed tasks, render with the "PVC" view.
if (maxPriority > 3 && isDone == true)
{
MyView = "PVC";
}
var items = await GetItemsAsync(maxPriority, isDone);
return View(MyView, items);
}

Copy the Views/Shared/Components/PriorityList/Default.cshtml file to a view named


Views/Shared/Components/PriorityList/PVC.cshtml . Add a heading to indicate the PVC

view is being used.

CSHTML

@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h2> PVC Named Priority Component View</h2>


<h4>@ViewBag.PriorityMessage</h4>
<ul>
@foreach (var todo in Model)
{
<li>@todo.Name</li>
}
</ul>

Run the app and verify PVC view.

If the PVC view isn't rendered, verify the view component with a priority of 4 or higher is
called.

Examine the view path


Change the priority parameter to three or less so the priority view isn't returned.

Temporarily rename the Views/ToDo/Components/PriorityList/Default.cshtml to


1Default.cshtml .
Test the app, the following error occurs:

txt

An unhandled exception occurred while processing the request.


InvalidOperationException: The view 'Components/PriorityList/Default'
wasn't found. The following locations were searched:
/Views/ToDo/Components/PriorityList/Default.cshtml
/Views/Shared/Components/PriorityList/Default.cshtml

Copy Views/ToDo/Components/PriorityList/1Default.cshtml to
Views/Shared/Components/PriorityList/Default.cshtml .

Add some markup to the Shared ToDo view component view to indicate the view is
from the Shared folder.

Test the Shared component view.

Avoid hard-coded strings


For compile time safety, replace the hard-coded view component name with the class
name. Update the PriorityListViewComponent.cs file to not use the "ViewComponent"
suffix:

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents;

public class PriorityList : ViewComponent


{
private readonly ToDoContext db;

public PriorityList(ToDoContext context)


{
db = context;
}

public async Task<IViewComponentResult> InvokeAsync(


int maxPriority, bool isDone)
{
var items = await GetItemsAsync(maxPriority, isDone);
return View(items);
}

private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)


{
return db!.ToDo!.Where(x => x.IsDone == isDone &&
x.Priority <= maxPriority).ToListAsync();
}
}

The view file:

CSHTML

</table>

<div>
Testing nameof(PriorityList) <br />

Maxium Priority: @ViewData["maxPriority"] <br />


Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync(nameof(PriorityList),
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>
An overload of Component.InvokeAsync method that takes a CLR type uses the typeof
operator:

CSHTML

</table>

<div>
Testing typeof(PriorityList) <br />

Maxium Priority: @ViewData["maxPriority"] <br />


Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync(typeof(PriorityList),
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>

Perform synchronous work


The framework handles invoking a synchronous Invoke method if asynchronous work
isn't required. The following method creates a synchronous Invoke view component:

C#

using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
public class PriorityListSync : ViewComponent
{
private readonly ToDoContext db;

public PriorityListSync(ToDoContext context)


{
db = context;
}

public IViewComponentResult Invoke(int maxPriority, bool isDone)


{

var x = db!.ToDo!.Where(x => x.IsDone == isDone &&


x.Priority <= maxPriority).ToList();
return View(x);
}
}
}

The view component's Razor file:

CSHTML

<div>
Testing nameof(PriorityList) <br />

Maxium Priority: @ViewData["maxPriority"] <br />


Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync(nameof(PriorityListSync),
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>

The view component is invoked in a Razor file (for example, Views/Home/Index.cshtml )


using one of the following approaches:

IViewComponentHelper
Tag Helper

To use the IViewComponentHelper approach, call Component.InvokeAsync :

CSHTML

@await Component.InvokeAsync(nameof(PriorityList),
new { maxPriority = 4, isDone = true })

To use the Tag Helper, register the assembly containing the View Component using the
@addTagHelper directive (the view component is in an assembly called MyWebApp ):

CSHTML

@addTagHelper *, MyWebApp

Use the view component Tag Helper in the Razor markup file:

CSHTML

<vc:priority-list max-priority="999" is-done="false">


</vc:priority-list>
The method signature of PriorityList.Invoke is synchronous, but Razor finds and calls
the method with Component.InvokeAsync in the markup file.

Additional resources
View or download sample code (how to download)
Dependency injection into views
View Components in Razor Pages
Why You Should Use View Components, not Partial Views, in ASP.NET Core
Razor file compilation in ASP.NET Core
Article • 03/20/2023

Razor files with a .cshtml extension are compiled at both build and publish time using
the Razor SDK. Runtime compilation may be optionally enabled by configuring the
project.

7 Note

Runtime compilation:

Isn't supported for Razor components of Blazor apps.


Doesn't support global using directives.
Doesn't support implicit using directives.
Disables .NET Hot Reload.
Is recommended for development, not for production.

Razor compilation
Build-time and publish-time compilation of Razor files is enabled by default by the
Razor SDK. When enabled, runtime compilation complements build-time compilation,
allowing Razor files to be updated if they're edited while the app is running.

Updating Razor views and Razor Pages during development while the app is running is
also supported using .NET Hot Reload.

7 Note

When enabled, runtime compilation disables .NET Hot Reload. We recommend


using Hot Reload instead of Razor runtime compilation during development.

Enable runtime compilation for all


environments
To enable runtime compilation for all environments:

1. Install the Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation NuGet


package.
2. Call AddRazorRuntimeCompilation in Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages()
.AddRazorRuntimeCompilation();

Enable runtime compilation conditionally


Runtime compilation can be enabled conditionally, which ensures that the published
output:

Uses compiled views.


Doesn't enable file watchers in production.

To enable runtime compilation only for the Development environment:

1. Install the Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation NuGet


package.

2. Call AddRazorRuntimeCompilation in Program.cs when the current environment is


set to Development:

C#

var builder = WebApplication.CreateBuilder(args);

var mvcBuilder = builder.Services.AddRazorPages();

if (builder.Environment.IsDevelopment())
{
mvcBuilder.AddRazorRuntimeCompilation();
}

Runtime compilation can also be enabled with a hosting startup assembly. To enable
runtime compilation in the Development environment for specific launch profiles:

1. Install the Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation NuGet


package.
2. Modify the launch profile's environmentVariables section in launchSettings.json :

Verify that ASPNETCORE_ENVIRONMENT is set to "Development" .


Set ASPNETCORE_HOSTINGSTARTUPASSEMBLIES to
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" . For example, the
following launchSettings.json enables runtime compilation for the
ViewCompilationSample and IIS Express launch profiles:

JSON

{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:7098",
"sslPort": 44332
}
},
"profiles": {
"ViewCompilationSample": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl":
"https://localhost:7173;http://localhost:5251",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES":
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES":
"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
}
}
}
}

With this approach, no code changes are needed in Program.cs . At runtime, ASP.NET
Core searches for an assembly-level HostingStartup attribute in
Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation . The HostingStartup attribute

specifies the app startup code to execute and that startup code enables runtime
compilation.
Enable runtime compilation for a Razor Class
Library
Consider a scenario in which a Razor Pages project references a Razor Class Library (RCL)
named MyClassLib. The RCL contains a _Layout.cshtml file consumed by MVC and Razor
Pages projects. To enable runtime compilation for the _Layout.cshtml file in that RCL,
make the following changes in the Razor Pages project:

1. Enable runtime compilation with the instructions at Enable runtime compilation


conditionally.

2. Configure MvcRazorRuntimeCompilationOptions in Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MvcRazorRuntimeCompilationOptions>(options
=>
{
var libraryPath = Path.GetFullPath(
Path.Combine(builder.Environment.ContentRootPath, "..",
"MyClassLib"));

options.FileProviders.Add(new PhysicalFileProvider(libraryPath));
});

The preceding code builds an absolute path to the MyClassLib RCL. The
PhysicalFileProvider API is used to locate directories and files at that absolute path.
Finally, the PhysicalFileProvider instance is added to a file providers collection,
which allows access to the RCL's .cshtml files.

Additional resources
RazorCompileOnBuild and RazorCompileOnPublish properties
Introduction to Razor Pages in ASP.NET Core
Views in ASP.NET Core MVC
ASP.NET Core Razor SDK
Display and Editor templates in ASP.NET
Core
Article • 08/30/2022

By Alexander Wicht

Display and Editor templates specify the user interface layout of custom types. Consider
the following Address model:

C#

public class Address


{
public int Id { get; set; }
public string FirstName { get; set; } = null!;
public string MiddleName { get; set; } = null!;
public string LastName { get; set; } = null!;
public string Street { get; set; } = null!;
public string City { get; set; } = null!;
public string State { get; set; } = null!;
public string Zipcode { get; set; } = null!;
}

A project that scaffolds the Address model displays the Address in the following form:
A web site could use a Display Template to show the Address in standard format:

Display and Editor templates can also reduce code duplication and maintenance costs.
Consider a web site that displays the Address model on 20 different pages. If the
Address model changes, the 20 pages will all need to be updated. If a Display Template

is used for the Address model, only the Display Template needs to be updated. For
example, the Address model might be updated to include the country.

Tag Helpers provide an alternative way that enables server-side code to participate in
creating and rendering HTML elements in Razor files. For more information, see Tag
Helpers compared to HTML Helpers.

Display templates
DisplayTemplates customize the display of model fields or create a layer of abstraction

between the model values and their display.

A DisplayTemplate is a Razor file placed in the DisplayTemplates folder:

For Razor Pages apps, in the Pages/Shared/DisplayTemplates folder.


For MVC apps, in the Views/Shared/DisplayTemplates folder or the
Views/ControllerName/DisplayTemplates folder. Display templates in the

Views/Shared/DisplayTemplates are used by all controllers in the app. Display

templates in the Views/ControllerName/DisplayTemplates folder are resolved only


by the ControllerName controller.

By convention, the DisplayTemplate file is named after the type to be displayed. The
Address.cshtml template used in this sample:
CSHTML

@model Address

<dl>
<dd>@Model.FirstName @Model.MiddleName @Model.LastName</dd>
<dd>@Model.Street</dd>
<dd>@Model.City @Model.State @Model.Zipcode</dd>
</dl>

The view engine automatically looks for a file in the DisplayTemplates folder that
matches the name of the type. If it doesn't find a matching template, it falls back to the
built in templates.

The following code shows the Details view of the scaffolded project:

CSHTML

@page
@model WebAddress.Pages.Adr.DetailsModel

@{
ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
<h4>Address</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Address.FirstName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address.FirstName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Address.MiddleName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address.MiddleName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Address.LastName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address.LastName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Address.Street)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address.Street)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Address.City)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address.City)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Address.State)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address.State)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Address.Zipcode)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address.Zipcode)
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Address?.Id">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>

The following code shows the Details view using the Address Display Template:

CSHTML

@page
@model WebAddress.Pages.Adr2.DetailsModel

@{
ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
<h4>Address DM</h4>
<hr />
<dl class="row">
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address)
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Address?.Id">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>

To reference a template whose name doesn't match the type name, use the
templateName parameter in the DisplayFor method. For example, the following markup
displays the Address model with the AddressShort template:

CSHTML

@page
@model WebAddress.Pages.Adr2.DetailsCCModel

@{
ViewData["Title"] = "Details Short";
}

<h1>Details Short</h1>

<div>
<h4>Address Short</h4>
<hr />
<dl class="row">
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Address,"AddressShort")
</dd>
</dl>
</div>

Use one of the available DisplayFor overloads that expose the additionalViewData
parameter to pass additional view data that is merged into the View Data Dictionary
instance created for the template.

Editor templates
Editor templates are used in form controls when the model is edited or updated.

An EditorTemplate is a Razor file placed in the EditorTemplates folder:

For Razor Pages apps, in the Pages/Shared/EditorTemplates folder.


For MVC apps, in the Views/Shared/EditorTemplates folder or the
Views/ControllerName/EditorTemplates folder.

The following markup shows the Pages/Shared/EditorTemplates/Address.cshtml used in


the sample:

CSHTML
@model Address

<dl>
<dd> Name:
<input asp-for="FirstName" /> <input asp-for="MiddleName" /> <input
asp-for="LastName" />
</dd>
<dd> Street:
<input asp-for="Street" />
</dd>

<dd> city/state/zip:
<input asp-for="City" /> <input asp-for="State" /> <input asp-
for="Zipcode" />
</dd>

</dl>

The following markup shows the Edit.cshtml page which uses the
Pages/Shared/EditorTemplates/Address.cshtml template:

CSHTML

@page
@model WebAddress.Pages.Adr.EditModel

@{
ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Address</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="Address.Id" />
@Html.EditorFor(model => model.Address)
<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");}
}

Additional resources
View or download sample code (how to download)
Tag Helpers
Tag Helpers compared to HTML Helpers
Upload files in ASP.NET Core
Article • 01/09/2023

By Rutger Storm

ASP.NET Core supports uploading one or more files using buffered model binding for
smaller files and unbuffered streaming for larger files.

View or download sample code (how to download)

Security considerations
Use caution when providing users with the ability to upload files to a server. Attackers
may attempt to:

Execute denial of service attacks.


Upload viruses or malware.
Compromise networks and servers in other ways.

Security steps that reduce the likelihood of a successful attack are:

Upload files to a dedicated file upload area, preferably to a non-system drive. A


dedicated location makes it easier to impose security restrictions on uploaded files.
Disable execute permissions on the file upload location.†
Do not persist uploaded files in the same directory tree as the app.†
Use a safe file name determined by the app. Don't use a file name provided by the
user or the untrusted file name of the uploaded file.† HTML encode the untrusted
file name when displaying it. For example, logging the file name or displaying in UI
(Razor automatically HTML encodes output).
Allow only approved file extensions for the app's design specification.†
Verify that client-side checks are performed on the server.† Client-side checks are
easy to circumvent.
Check the size of an uploaded file. Set a maximum size limit to prevent large
uploads.†
When files shouldn't be overwritten by an uploaded file with the same name, check
the file name against the database or physical storage before uploading the file.
Run a virus/malware scanner on uploaded content before the file is stored.

†The sample app demonstrates an approach that meets the criteria.

2 Warning
Uploading malicious code to a system is frequently the first step to executing code
that can:

Completely gain control of a system.


Overload a system with the result that the system crashes.
Compromise user or system data.
Apply graffiti to a public UI.

For information on reducing the attack surface area when accepting files from
users, see the following resources:

Unrestricted File Upload


Azure Security: Ensure appropriate controls are in place when accepting
files from users

For more information on implementing security measures, including examples from the
sample app, see the Validation section.

Storage scenarios
Common storage options for files include:

Database
For small file uploads, a database is often faster than physical storage (file
system or network share) options.
A database is often more convenient than physical storage options because
retrieval of a database record for user data can concurrently supply the file
content (for example, an avatar image).
A database is potentially less expensive than using a cloud data storage service.

Physical storage (file system or network share)


For large file uploads:
Database limits may restrict the size of the upload.
Physical storage is often less economical than storage in a database.
Physical storage is potentially less expensive than using a cloud data storage
service.
The app's process must have read and write permissions to the storage location.
Never grant execute permission.

Cloud data storage service, for example, Azure Blob Storage .


Services usually offer improved scalability and resiliency over on-premises
solutions that are usually subject to single points of failure.
Services are potentially lower cost in large storage infrastructure scenarios.

For more information, see Quickstart: Use .NET to create a blob in object storage.

Small and large files


The definition of small and large files depend on the computing resources available.
Apps should benchmark the storage approach used to ensure it can handle the
expected sizes. Benchmark memory, CPU, disk, and database performance.

While specific boundaries can't be provided on what is small vs large for your
deployment, here are some of AspNetCore's related defaults for FormOptions :

By default, HttpRequest.Form does not buffer the entire request body (BufferBody),
but it does buffer any multipart form files included.
MultipartBodyLengthLimit is the max size for buffered form files, defaults to
128MB.
MemoryBufferThreshold indicates how much to buffer files in memory before
transitioning to a buffer file on disk, defaults to 64KB. MemoryBufferThreshold acts
as a boundary between small and large files which is raised or lowered depending
on the apps resources and scenarios.

Fore more information on FormOptions , see the source code .

File upload scenarios


Two general approaches for uploading files are buffering and streaming.

Buffering

The entire file is read into an IFormFile. IFormFile is a C# representation of the file used
to process or save the file.

The disk and memory used by file uploads depend on the number and size of
concurrent file uploads. If an app attempts to buffer too many uploads, the site crashes
when it runs out of memory or disk space. If the size or frequency of file uploads is
exhausting app resources, use streaming.

Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.
Temporary files for larger requests are written to the location named in the
ASPNETCORE_TEMP environment variable. If ASPNETCORE_TEMP is not defined, the files are
written to the current user's temporary folder.

Buffering small files is covered in the following sections of this topic:

Physical storage
Database

Streaming

The file is received from a multipart request and directly processed or saved by the app.
Streaming doesn't improve performance significantly. Streaming reduces the demands
for memory or disk space when uploading files.

Streaming large files is covered in the Upload large files with streaming section.

Upload small files with buffered model binding to


physical storage
To upload small files, use a multipart form or construct a POST request using JavaScript.

The following example demonstrates the use of a Razor Pages form to upload a single
file ( Pages/BufferedSingleFileUploadPhysical.cshtml in the sample app):

CSHTML

<form enctype="multipart/form-data" method="post">


<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit"
value="Upload" />
</form>

The following example is analogous to the prior example except that:

JavaScript's (Fetch API ) is used to submit the form's data.


There's no validation.
CSHTML

<form action="BufferedSingleFileUploadPhysical/?handler=Upload"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return
false;"
method="post">
<dl>
<dt>
<label for="FileUpload_FormFile">File</label>
</dt>
<dd>
<input id="FileUpload_FormFile" type="file"
name="FileUpload.FormFile" />
</dd>
</dl>

<input class="btn" type="submit" value="Upload" />

<div style="margin-top:15px">
<output name="result"></output>
</div>
</form>

<script>
async function AJAXSubmit (oFormElement) {
var resultElement = oFormElement.elements.namedItem("result");
const formData = new FormData(oFormElement);

try {
const response = await fetch(oFormElement.action, {
method: 'POST',
body: formData
});

if (response.ok) {
window.location.href = '/';
}

resultElement.value = 'Result: ' + response.status + ' ' +


response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
</script>

To perform the form POST in JavaScript for clients that don't support the Fetch API ,
use one of the following approaches:

Use a Fetch Polyfill (for example, window.fetch polyfill (github/fetch) ).

Use XMLHttpRequest . For example:


JavaScript

<script>
"use strict";

function AJAXSubmit (oFormElement) {


var oReq = new XMLHttpRequest();
oReq.onload = function(e) {
oFormElement.elements.namedItem("result").value =
'Result: ' + this.status + ' ' + this.statusText;
};
oReq.open("post", oFormElement.action);
oReq.send(new FormData(oFormElement));
}
</script>

In order to support file uploads, HTML forms must specify an encoding type ( enctype )
of multipart/form-data .

For a files input element to support uploading multiple files provide the multiple
attribute on the <input> element:

CSHTML

<input asp-for="FileUpload.FormFiles" type="file" multiple>

The individual files uploaded to the server can be accessed through Model Binding
using IFormFile. The sample app demonstrates multiple buffered file uploads for
database and physical storage scenarios.

2 Warning

Do not use the FileName property of IFormFile other than for display and logging.
When displaying or logging, HTML encode the file name. An attacker can provide a
malicious filename, including full paths or relative paths. Applications should:

Remove the path from the user-supplied filename.


Save the HTML-encoded, path-removed filename for UI or logging.
Generate a new random filename for storage.

The following code removes the path from the file name:

C#

string untrustedFileName = Path.GetFileName(pathName);


The examples provided thus far don't take into account security considerations.
Additional information is provided by the following sections and the sample app :

Security considerations
Validation

When uploading files using model binding and IFormFile, the action method can accept:

A single IFormFile.
Any of the following collections that represent several files:
IFormFileCollection
IEnumerable<IFormFile>
List<IFormFile>

7 Note

Binding matches form files by name. For example, the HTML name value in <input
type="file" name="formFile"> must match the C# parameter/property bound

( FormFile ). For more information, see the Match name attribute value to
parameter name of POST method section.

The following example:

Loops through one or more uploaded files.


Uses Path.GetTempFileName to return a full path for a file, including the file name.
Saves the files to the local file system using a file name generated by the app.
Returns the total number and size of files uploaded.

C#

public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)


{
long size = files.Sum(f => f.Length);

foreach (var formFile in files)


{
if (formFile.Length > 0)
{
var filePath = Path.GetTempFileName();

using (var stream = System.IO.File.Create(filePath))


{
await formFile.CopyToAsync(stream);
}
}
}

// Process uploaded files


// Don't rely on or trust the FileName property without validation.

return Ok(new { count = files.Count, size });


}

Use Path.GetRandomFileName to generate a file name without a path. In the following


example, the path is obtained from configuration:

C#

foreach (var formFile in files)


{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());

using (var stream = System.IO.File.Create(filePath))


{
await formFile.CopyToAsync(stream);
}
}
}

The path passed to the FileStream must include the file name. If the file name isn't
provided, an UnauthorizedAccessException is thrown at runtime.

Files uploaded using the IFormFile technique are buffered in memory or on disk on the
server before processing. Inside the action method, the IFormFile contents are accessible
as a Stream. In addition to the local file system, files can be saved to a network share or
to a file storage service, such as Azure Blob storage.

For another example that loops over multiple files for upload and uses safe file names,
see Pages/BufferedMultipleFileUploadPhysical.cshtml.cs in the sample app.

2 Warning

Path.GetTempFileName throws an IOException if more than 65,535 files are


created without deleting previous temporary files. The limit of 65,535 files is a per-
server limit. For more information on this limit on Windows OS, see the remarks in
the following topics:

GetTempFileNameA function
GetTempFileName

Upload small files with buffered model binding to a


database
To store binary file data in a database using Entity Framework, define a Byte array
property on the entity:

C#

public class AppFile


{
public int Id { get; set; }
public byte[] Content { get; set; }
}

Specify a page model property for the class that includes an IFormFile:

C#

public class BufferedSingleFileUploadDbModel : PageModel


{
...

[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }

...
}

public class BufferedSingleFileUploadDb


{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}

7 Note

IFormFile can be used directly as an action method parameter or as a bound model


property. The prior example uses a bound model property.

The FileUpload is used in the Razor Pages form:

CSHTML
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit"
value="Upload">
</form>

When the form is POSTed to the server, copy the IFormFile to a stream and save it as a
byte array in the database. In the following example, _dbContext stores the app's
database context:

C#

public async Task<IActionResult> OnPostUploadAsync()


{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);

// Upload the file if less than 2 MB


if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};

_dbContext.File.Add(file);

await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}

return Page();
}

The preceding example is similar to a scenario demonstrated in the sample app:

Pages/BufferedSingleFileUploadDb.cshtml

Pages/BufferedSingleFileUploadDb.cshtml.cs
2 Warning

Use caution when storing binary data in relational databases, as it can adversely
impact performance.

Don't rely on or trust the FileName property of IFormFile without validation. The
FileName property should only be used for display purposes and only after HTML

encoding.

The examples provided don't take into account security considerations. Additional
information is provided by the following sections and the sample app :

Security considerations
Validation

Upload large files with streaming


The 3.1 example demonstrates how to use JavaScript to stream a file to a controller
action. The file's antiforgery token is generated using a custom filter attribute and
passed to the client HTTP headers instead of in the request body. Because the action
method processes the uploaded data directly, form model binding is disabled by
another custom filter. Within the action, the form's contents are read using a
MultipartReader , which reads each individual MultipartSection , processing the file or

storing the contents as appropriate. After the multipart sections are read, the action
performs its own model binding.

The initial page response loads the form and saves an antiforgery token in a cookie (via
the GenerateAntiforgeryTokenCookieAttribute attribute). The attribute uses ASP.NET
Core's built-in antiforgery support to set a cookie with a request token:

C#

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute


{
public override void OnResultExecuting(ResultExecutingContext context)
{
var antiforgery =
context.HttpContext.RequestServices.GetService<IAntiforgery>();

// Send the request token as a JavaScript-readable cookie


var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}

public override void OnResultExecuted(ResultExecutedContext context)


{
}
}

The DisableFormValueModelBindingAttribute is used to disable model binding:

C#

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute,
IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}

public void OnResourceExecuted(ResourceExecutedContext context)


{
}
}

In the sample app, GenerateAntiforgeryTokenCookieAttribute and


DisableFormValueModelBindingAttribute are applied as filters to the page application
models of /StreamedSingleFileUploadDb and /StreamedSingleFileUploadPhysical in
Startup.ConfigureServices using Razor Pages conventions:

C#

services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
});

Since model binding doesn't read the form, parameters that are bound from the form
don't bind (query, route, and header continue to work). The action method works
directly with the Request property. A MultipartReader is used to read each section.
Key/value data is stored in a KeyValueAccumulator . After the multipart sections are read,
the contents of the KeyValueAccumulator are used to bind the form data to a model
type.

The complete StreamingController.UploadDatabase method for streaming to a database


with EF Core:

C#

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error

return BadRequest(ModelState);
}

// Accumulate the form data key-value pairs in the request


(formAccumulator).
var formAccumulator = new KeyValueAccumulator();
var trustedFileNameForDisplay = string.Empty;
var untrustedFileNameForStorage = string.Empty;
var streamedFileContent = Array.Empty<byte>();

var boundary = MultipartRequestHelper.GetBoundary(


MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);

var section = await reader.ReadNextSectionAsync();


while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);

if (hasContentDispositionHeader)
{
if (MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
untrustedFileNameForStorage =
contentDisposition.FileName.Value;
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);

streamedFileContent =
await FileHelpers.ProcessStreamedFile(section,
contentDisposition,
ModelState, _permittedExtensions, _fileSizeLimit);

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
else if (MultipartRequestHelper
.HasFormDataContentDisposition(contentDisposition))
{
// Don't limit the key name length because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities
.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);

if (encoding == null)
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error

return BadRequest(ModelState);
}

using (var streamReader = new StreamReader(


section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by
// MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();

if (string.Equals(value, "undefined",
StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}

formAccumulator.Append(key, value);

if (formAccumulator.ValueCount >
_defaultFormOptions.ValueCountLimit)
{
// Form key count limit of
// _defaultFormOptions.ValueCountLimit
// is exceeded.
ModelState.AddModelError("File",
$"The request couldn't be processed (Error
3).");
// Log error

return BadRequest(ModelState);
}
}
}
}

// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}

// Bind form data to the model


var formData = new FormData();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
valueProvider: formValueProvider);

if (!bindingSuccessful)
{
ModelState.AddModelError("File",
"The request couldn't be processed (Error 5).");
// Log error

return BadRequest(ModelState);
}

// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample app.

var file = new AppFile()


{
Content = streamedFileContent,
UntrustedName = untrustedFileNameForStorage,
Note = formData.Note,
Size = streamedFileContent.Length,
UploadDT = DateTime.UtcNow
};

_context.File.Add(file);
await _context.SaveChangesAsync();

return Created(nameof(StreamingController), null);


}

MultipartRequestHelper ( Utilities/MultipartRequestHelper.cs ):

C#

using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----
WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec at https://tools.ietf.org/html/rfc2046#section-5.1
states that 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType,
int lengthLimit)
{
var boundary =
HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type
boundary.");
}

if (boundary.Length > lengthLimit)


{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit}
exceeded.");
}
return boundary;
}

public static bool IsMultipartContentType(string contentType)


{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/",
StringComparison.OrdinalIgnoreCase) >= 0;
}

public static bool


HasFormDataContentDisposition(ContentDispositionHeaderValue
contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&&
string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}

public static bool


HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1";
filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
||
!string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
}

The complete StreamingController.UploadPhysical method for streaming to a physical


location:

C#

[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}

var boundary = MultipartRequestHelper.GetBoundary(


MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();

while (section != null)


{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);

if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error

return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage =
Path.GetRandomFileName();

// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.

var streamedFileContent = await


FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

using (var targetStream = System.IO.File.Create(


Path.Combine(_targetFilePath,
trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);

_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved
to " +
"'{TargetFilePath}' as
{TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}

// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}

return Created(nameof(StreamingController), null);


}

In the sample app, validation checks are handled by FileHelpers.ProcessStreamedFile .

Validation
The sample app's FileHelpers class demonstrates several checks for buffered IFormFile
and streamed file uploads. For processing IFormFile buffered file uploads in the sample
app, see the ProcessFormFile method in the Utilities/FileHelpers.cs file. For
processing streamed files, see the ProcessStreamedFile method in the same file.

2 Warning

The validation processing methods demonstrated in the sample app don't scan the
content of uploaded files. In most production scenarios, a virus/malware scanner
API is used on the file before making the file available to users or other systems.

Although the topic sample provides a working example of validation techniques,


don't implement the FileHelpers class in a production app unless you:

Fully understand the implementation.


Modify the implementation as appropriate for the app's environment and
specifications.

Never indiscriminately implement security code in an app without addressing


these requirements.

Content validation
Use a third party virus/malware scanning API on uploaded content.

Scanning files is demanding on server resources in high volume scenarios. If request


processing performance is diminished due to file scanning, consider offloading the
scanning work to a background service, possibly a service running on a server different
from the app's server. Typically, uploaded files are held in a quarantined area until the
background virus scanner checks them. When a file passes, the file is moved to the
normal file storage location. These steps are usually performed in conjunction with a
database record that indicates the scanning status of a file. By using such an approach,
the app and app server remain focused on responding to requests.

File extension validation


The uploaded file's extension should be checked against a list of permitted extensions.
For example:

C#

private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// The extension is invalid ... discontinue processing the file
}

File signature validation


A file's signature is determined by the first few bytes at the start of a file. These bytes
can be used to indicate if the extension matches the content of the file. The sample app
checks file signatures for a few common file types. In the following example, the file
signature for a JPEG image is checked against the file:
C#

private static readonly Dictionary<string, List<byte[]>> _fileSignature =


new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};

using (var reader = new BinaryReader(uploadedFileData))


{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

return signatures.Any(signature =>


headerBytes.Take(signature.Length).SequenceEqual(signature));
}

To obtain additional file signatures, use a file signatures database (Google search
result) and official file specifications. Consulting official file specifications may ensure
that the selected signatures are valid.

File name security


Never use a client-supplied file name for saving a file to physical storage. Create a safe
file name for the file using Path.GetRandomFileName or Path.GetTempFileName to
create a full path (including the file name) for temporary storage.

Razor automatically HTML encodes property values for display. The following code is
safe to use:

CSHTML

@foreach (var file in Model.DatabaseFiles) {


<tr>
<td>
@file.UntrustedName
</td>
</tr>
}

Outside of Razor, always HtmlEncode file name content from a user's request.
Many implementations must include a check that the file exists; otherwise, the file is
overwritten by a file of the same name. Supply additional logic to meet your app's
specifications.

Size validation
Limit the size of uploaded files.

In the sample app, the size of the file is limited to 2 MB (indicated in bytes). The limit is
supplied via Configuration from the appsettings.json file:

JSON

{
"FileSizeLimit": 2097152
}

The FileSizeLimit is injected into PageModel classes:

C#

public class BufferedSingleFileUploadPhysicalModel : PageModel


{
private readonly long _fileSizeLimit;

public BufferedSingleFileUploadPhysicalModel(IConfiguration config)


{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}

...
}

When a file size exceeds the limit, the file is rejected:

C#

if (formFile.Length > _fileSizeLimit)


{
// The file is too large ... discontinue processing the file
}

Match name attribute value to parameter name of POST


method
In non-Razor forms that POST form data or use JavaScript's FormData directly, the name
specified in the form's element or FormData must match the name of the parameter in
the controller's action.

In the following example:

When using an <input> element, the name attribute is set to the value
battlePlans :

HTML

<input type="file" name="battlePlans" multiple>

When using FormData in JavaScript, the name is set to the value battlePlans :

JavaScript

var formData = new FormData();

for (var file in files) {


formData.append("battlePlans", file, file.name);
}

Use a matching name for the parameter of the C# method ( battlePlans ):

For a Razor Pages page handler method named Upload :

C#

public async Task<IActionResult> OnPostUploadAsync(List<IFormFile>


battlePlans)

For an MVC POST controller action method:

C#

public async Task<IActionResult> Post(List<IFormFile> battlePlans)

Server and app configuration

Multipart body length limit


MultipartBodyLengthLimit sets the limit for the length of each multipart body. Form
sections that exceed this limit throw an InvalidDataException when parsed. The default is
134,217,728 (128 MB). Customize the limit using the MultipartBodyLengthLimit setting
in Startup.ConfigureServices :

C#

public void ConfigureServices(IServiceCollection services)


{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}

RequestFormLimitsAttribute is used to set the MultipartBodyLengthLimit for a single


page or action.

In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices :

C#

services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
});

In a Razor Pages app or an MVC app, apply the filter to the page model or action
method:

C#

// Set the limit to 256 MB


[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Kestrel maximum request body size
For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes,
which is approximately 28.6 MB. Customize the limit using the MaxRequestBodySize
Kestrel server option:

C#

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


Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel((context, options) =>
{
// Handle requests up to 50 MB
options.Limits.MaxRequestBodySize = 52428800;
})
.UseStartup<Startup>();
});

RequestSizeLimitAttribute is used to set the MaxRequestBodySize for a single page or


action.

In a Razor Pages app, apply the filter with a convention in Startup.ConfigureServices :

C#

services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
});

In a Razor pages app or an MVC app, apply the filter to the page handler class or action
method:

C#

// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}

The RequestSizeLimitAttribute can also be applied using the @attribute Razor


directive:

CSHTML

@attribute [RequestSizeLimitAttribute(52428800)]

Other Kestrel limits


Other Kestrel limits may apply for apps hosted by Kestrel:

Maximum client connections


Request and response data rates

IIS
The default request limit ( maxAllowedContentLength ) is 30,000,000 bytes, which is
approximately 28.6 MB. Customize the limit in the web.config file. In the following
example, the limit is set to 50 MB (52,428,800 bytes):

XML

<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>

The maxAllowedContentLength setting only applies to IIS. For more information, see
Request Limits <requestLimits>.

Troubleshoot
Below are some common problems encountered when working with uploading files and
their possible solutions.

Not Found error when deployed to an IIS server


The following error indicates that the uploaded file exceeds the server's configured
content length:

HTTP 404.13 - Not Found


The request filtering module is configured to deny a request that exceeds
the request content length.

For more information, see the IIS section.

Connection failure
A connection error and a reset server connection probably indicates that the uploaded
file exceeds Kestrel's maximum request body size. For more information, see the Kestrel
maximum request body size section. Kestrel client connection limits may also require
adjustment.

Null Reference Exception with IFormFile


If the controller is accepting uploaded files using IFormFile but the value is null ,
confirm that the HTML form is specifying an enctype value of multipart/form-data . If
this attribute isn't set on the <form> element, the file upload doesn't occur and any
bound IFormFile arguments are null . Also confirm that the upload naming in form data
matches the app's naming.

Stream was too long


The examples in this topic rely upon MemoryStream to hold the uploaded file's content.
The size limit of a MemoryStream is int.MaxValue . If the app's file upload scenario
requires holding file content larger than 50 MB, use an alternative approach that doesn't
rely upon a single MemoryStream for holding an uploaded file's content.

Additional resources
HTTP connection request draining

Unrestricted File Upload


Azure Security: Security Frame: Input Validation | Mitigations
Azure Cloud Design Patterns: Valet Key pattern
ASP.NET Core Web SDK
Article • 04/11/2023

Overview
Microsoft.NET.Sdk.Web is an MSBuild project SDK for building ASP.NET Core apps. It's

possible to build an ASP.NET Core app without this SDK, however, the Web SDK is:

Tailored towards providing a first-class experience.


The recommended target for most users.

Use the Web.SDK in a project:

XML

<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- omitted for brevity -->
</Project>

Features enabled by using the Web SDK:

Implicitly references:
The ASP.NET Core shared framework.
Analyzers designed for building ASP.NET Core apps.

The Web SDK imports MSBuild targets that enable the use of publish profiles and
publishing using WebDeploy.

Properties
Property Description

DisableImplicitFrameworkReferences Disables implicit reference to the


Microsoft.AspNetCore.App shared framework.

DisableImplicitAspNetCoreAnalyzers Disables implicit reference to ASP.NET Core analyzers.

DisableImplicitComponentsAnalyzers Disables implicit reference to Razor Components analyzers


when building Blazor (server) applications.

For more information on tasks, targets, properties, implicit blobs, globs, publishing,
methods, and more, see the README file in the WebSdk repository.
dotnet aspnet-codegenerator
Article • 12/06/2022

By Rick Anderson

dotnet aspnet-codegenerator - Runs the ASP.NET Core scaffolding engine. dotnet

aspnet-codegenerator is only required to scaffold from the command line, it's not
needed to use scaffolding with Visual Studio.

Install and update aspnet-codegenerator


Install the .NET SDK .

dotnet aspnet-codegenerator is a global tool that must be installed. The following

command installs the latest stable version of the dotnet aspnet-codegenerator tool:

.NET CLI

dotnet tool install -g dotnet-aspnet-codegenerator

The following command updates dotnet aspnet-codegenerator to the latest stable


version available from the installed .NET Core SDKs:

.NET CLI

dotnet tool update -g dotnet-aspnet-codegenerator

Uninstall aspnet-codegenerator
It may be necessary to uninstall the aspnet-codegenerator to resolve problems. For
example, if you installed a preview version of aspnet-codegenerator , uninstall it before
installing the released version.

The following commands uninstall the dotnet aspnet-codegenerator tool and installs the
latest stable version:

.NET CLI

dotnet tool uninstall -g dotnet-aspnet-codegenerator


dotnet tool install -g dotnet-aspnet-codegenerator
Synopsis

dotnet aspnet-codegenerator [arguments] [-p|--project] [-n|--nuget-package-


dir] [-c|--configuration] [-tfm|--target-framework] [-b|--build-base-path]
[--no-build]
dotnet aspnet-codegenerator [-h|--help]

Description
The dotnet aspnet-codegenerator global command runs the ASP.NET Core code
generator and scaffolding engine.

Arguments
generator

The code generator to run. The following generators are available:

Generator Operation

area Scaffolds an Area

controller Scaffolds a controller

identity Scaffolds Identity

razorpage Scaffolds Razor Pages

view Scaffolds a view

Options
-n|--nuget-package-dir

Specifies the NuGet package directory.

-c|--configuration {Debug|Release}

Defines the build configuration. The default value is Debug .

-tfm|--target-framework
Target Framework to use. For example, net46 .

-b|--build-base-path

The build base path.

-h|--help

Prints out a short help for the command.

--no-build

Doesn't build the project before running. It also implicitly sets the --no-restore flag.

-p|--project <PATH>

Specifies the path of the project file to run (folder name or full path). If not specified, it
defaults to the current directory.

Generator options
The following sections detail the options available for the supported generators:

Area
Controller
Identity
Razorpage
View

Area options
This tool is intended for ASP.NET Core web projects with controllers and views. It's not
intended for Razor Pages apps.

Usage: dotnet aspnet-codegenerator area AreaNameToGenerate

The preceding command generates the following folders:

Areas
AreaNameToGenerate
Controllers
Data
Models
Views
Controller options
The following table lists options for aspnet-codegenerator razorpage , controller and
view :

Option Description

--model or -m Model class to use.

--dataContext or -dc The DbContext class to use or the name of the class to generate.

--bootstrapVersion or - Specifies the bootstrap version. Valid values are 3 or 4 . Default is 4 . If


b needed and not present, a wwwroot directory is created that includes
the bootstrap files of the specified version.

-- Reference script libraries in the generated views. Adds


referenceScriptLibraries _ValidationScriptsPartial to Edit and Create pages.
or -scripts

--layout or -l Custom Layout page to use.

--useDefaultLayout or - Use the default layout for the views.


udl

--force or -f Overwrite existing files.

--relativeFolderPath or Specify the relative output folder path from project where the file needs
-outDir to be generated, if not specified, file will be generated in the project
folder

--useSqlite or -sqlite Flag to specify if DbContext should use SQLite instead of SQL Server.

The following table lists options unique to aspnet-codegenerator controller :

Option Description

--controllerName or Name of the controller.


-name

--useAsyncActions Generate async controller actions.


or -async

--noViews or -nv Generate no views.

--restWithNoViews Generate a Controller with REST style API. noViews is assumed and any
or -api view related options are ignored.

--readWriteActions Generate controller with read/write actions without a model.


or -actions
Use the -h switch for help on the aspnet-codegenerator controller command:

.NET CLI

dotnet aspnet-codegenerator controller -h

See Scaffold the movie model for an example of dotnet aspnet-codegenerator


controller .

Razorpage
Razor Pages can be individually scaffolded by specifying the name of the new page and
the template to use. The supported templates are:

Empty
Create

Edit

Delete
Details

List

For example, the following command uses the Edit template to generate MyEdit.cshtml
and MyEdit.cshtml.cs :

.NET CLI

dotnet aspnet-codegenerator razorpage MyEdit Edit -m Movie -dc


RazorPagesMovieContext -outDir Pages/Movies

Typically, the template and generated file name is not specified, and the following
templates are created:

Create
Edit

Delete

Details
List

The following table lists options for aspnet-codegenerator razorpage , controller and
view :
Option Description

--model or -m Model class to use.

--dataContext or -dc The DbContext class to use or the name of the class to generate.

--bootstrapVersion or - Specifies the bootstrap version. Valid values are 3 or 4 . Default is 4 . If


b needed and not present, a wwwroot directory is created that includes
the bootstrap files of the specified version.

-- Reference script libraries in the generated views. Adds


referenceScriptLibraries _ValidationScriptsPartial to Edit and Create pages.
or -scripts

--layout or -l Custom Layout page to use.

--useDefaultLayout or - Use the default layout for the views.


udl

--force or -f Overwrite existing files.

--relativeFolderPath or Specify the relative output folder path from project where the file needs
-outDir to be generated, if not specified, file will be generated in the project
folder

--useSqlite or -sqlite Flag to specify if DbContext should use SQLite instead of SQL Server.

The following table lists options unique to aspnet-codegenerator razorpage :

Option Description

--namespaceName or - The name of the namespace to use for the generated PageModel
namespace

--partialView or -partial Generate a partial view. Layout options -l and -udl are ignored if
this is specified.

--noPageModel or -npm Switch to not generate a PageModel class for Empty template

Use the -h switch for help on the aspnet-codegenerator razorpage command:

.NET CLI

dotnet aspnet-codegenerator razorpage -h

See Scaffold the movie model for an example of dotnet aspnet-codegenerator


razorpage .
View
Views can be individually scaffolded by specifying the name of the view and the
template to use. The supported templates are:

Empty

Create
Edit

Delete
Details

List

For example, the following command uses the Edit template to generate MyEdit.cshtml :

.NET CLI

dotnet aspnet-codegenerator view MyEdit Edit -m Movie -dc MovieContext -


outDir Views/Movies

The following table lists options for aspnet-codegenerator razorpage , controller and
view :

Option Description

--model or -m Model class to use.

--dataContext or -dc The DbContext class to use or the name of the class to generate.

--bootstrapVersion or - Specifies the bootstrap version. Valid values are 3 or 4 . Default is 4 . If


b needed and not present, a wwwroot directory is created that includes
the bootstrap files of the specified version.

-- Reference script libraries in the generated views. Adds


referenceScriptLibraries _ValidationScriptsPartial to Edit and Create pages.
or -scripts

--layout or -l Custom Layout page to use.

--useDefaultLayout or - Use the default layout for the views.


udl

--force or -f Overwrite existing files.

--relativeFolderPath or Specify the relative output folder path from project where the file needs
-outDir to be generated, if not specified, file will be generated in the project
folder
Option Description

--useSqlite or -sqlite Flag to specify if DbContext should use SQLite instead of SQL Server.

The following table lists options unique to aspnet-codegenerator view :

Option Description

--controllerNamespace or - Specify the name of the namespace to use for the generated
namespace controller

--partialView or -partial Generate a partial view, other layout options (-l and -udl) are
ignored if this is specified

Use the -h switch for help on the aspnet-codegenerator view command:

.NET CLI

dotnet aspnet-codegenerator view -h

Identity
See Scaffold Identity
Choose between controller-based APIs
and minimal APIs
Article • 04/11/2023

ASP.NET Core supports two approaches to creating APIs: a controller-based approach


and minimal APIs. Controllers in an API project are classes that derive from
ControllerBase. Minimal APIs define endpoints with logical handlers in lambdas or
methods. This article points out differences between the two approaches.

The design of minimal APIs hides the host class by default and focuses on configuration
and extensibility via extension methods that take functions as lambda expressions.
Controllers are classes that can take dependencies via constructor injection or property
injection, and generally follow object-oriented patterns. Minimal APIs support
dependency injection through other approaches such as accessing the service provider.

Here's sample code for an API based on controllers:

C#

namespace APIWithControllers;

public class Program


{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
var app = builder.Build();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();
}
}

C#

using Microsoft.AspNetCore.Mvc;

namespace APIWithControllers.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy",
"Hot", "Sweltering", "Scorching"
};

private readonly ILogger<WeatherForecastController> _logger;

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

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

The following code provides the same functionality in a minimal API project. Notice that
the minimal API approach involves including the related code in lambda expressions.

C#

namespace MinimalAPI;

public class Program


{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseHttpsRedirection();

var summaries = new[]


{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
"Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", (HttpContext httpContext) =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date =
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary =
summaries[Random.Shared.Next(summaries.Length)]
})
.ToArray();
return forecast;
});

app.Run();
}
}

Both API projects refer to the following class:

C#

namespace APIWithControllers;

public class WeatherForecast


{
public DateOnly Date { get; set; }

public int TemperatureC { get; set; }

public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

public string? Summary { get; set; }


}

Minimal APIs have many of the same capabilities as controller-based APIs. They support
the configuration and customization needed to scale to multiple APIs, handle complex
routes, apply authorization rules, and control the content of API responses. There are a
few capabilities available with controller-based APIs that are not yet supported or
implemented by minimal APIs. These include:

No built-in support for model binding (IModelBinderProvider, IModelBinder).


Support can be added with a custom binding shim.
No built-in support for validation (IModelValidator).
No support for application parts or the application model. There's no way to apply
or build your own conventions.
No built-in view rendering support. We recommend using Razor Pages for
rendering views.
No support for JsonPatch
No support for OData

See also
Create web APIs with ASP.NET Core.
Tutorial: Create a web API with ASP.NET Core
Minimal APIs overview
Tutorial: Create a minimal API with ASP.NET Core
Create web APIs with ASP.NET Core
Article • 04/11/2023

ASP.NET Core supports creating web APIs using controllers or using minimal APIs.
Controllers in a web API are classes that derive from ControllerBase. Controllers are
activated and disposed on a per request basis.

This article shows how to use controllers for handling web API requests. For information
on creating web APIs without controllers, see Tutorial: Create a minimal API with
ASP.NET Core.

ControllerBase class
A controller-based web API consists of one or more controller classes that derive from
ControllerBase. The web API project template provides a starter controller:

C#

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase

Web API controllers should typically derive from ControllerBase rather from Controller.
Controller derives from ControllerBase and adds support for views, so it's for handling

web pages, not web API requests. If the same controller must support views and web
APIs, derive from Controller .

The ControllerBase class provides many properties and methods that are useful for
handling HTTP requests. For example, CreatedAtAction returns a 201 status code:

C#

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
pet.Id = _petsInMemoryStore.Any() ?
_petsInMemoryStore.Max(p => p.Id) + 1 : 1;
_petsInMemoryStore.Add(pet);

return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);


}
The following table contains examples of methods in ControllerBase .

Method Notes

BadRequest Returns 400 status code.

NotFound Returns 404 status code.

PhysicalFile Returns a file.

TryUpdateModelAsync Invokes model binding.

TryValidateModel Invokes model validation.

For a list of all available methods and properties, see ControllerBase.

Attributes
The Microsoft.AspNetCore.Mvc namespace provides attributes that can be used to
configure the behavior of web API controllers and action methods. The following
example uses attributes to specify the supported HTTP action verb and any known HTTP
status codes that could be returned:

C#

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
pet.Id = _petsInMemoryStore.Any() ?
_petsInMemoryStore.Max(p => p.Id) + 1 : 1;
_petsInMemoryStore.Add(pet);

return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);


}

Here are some more examples of attributes that are available.

Attribute Notes

[Route] Specifies URL pattern for a controller or action.

[Bind] Specifies prefix and properties to include for model binding.

[HttpGet] Identifies an action that supports the HTTP GET action verb.

[Consumes] Specifies data types that an action accepts.


Attribute Notes

[Produces] Specifies data types that an action returns.

For a list that includes the available attributes, see the Microsoft.AspNetCore.Mvc
namespace.

ApiController attribute
The [ApiController] attribute can be applied to a controller class to enable the following
opinionated, API-specific behaviors:

Attribute routing requirement


Automatic HTTP 400 responses
Binding source parameter inference
Multipart/form-data request inference
Problem details for error status codes

Attribute on specific controllers


The [ApiController] attribute can be applied to specific controllers, as in the following
example from the project template:

C#

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase

Attribute on multiple controllers


One approach to using the attribute on more than one controller is to create a custom
base controller class annotated with the [ApiController] attribute. The following example
shows a custom base class and a controller that derives from it:

C#

[ApiController]
public class MyControllerBase : ControllerBase
{
}
C#

[Produces(MediaTypeNames.Application.Json)]
[Route("[controller]")]
public class PetsController : MyControllerBase

Attribute on an assembly
The [ApiController] attribute can be applied to an assembly. When the
[ApiController] attribute is applied to an assembly, all controllers in the assembly have

the [ApiController] attribute applied. There's no way to opt out for individual
controllers. Apply the assembly-level attribute to the Program.cs file:

C#

using Microsoft.AspNetCore.Mvc;
[assembly: ApiController]
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Attribute routing requirement


The [ApiController] attribute makes attribute routing a requirement. For example:

C#

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase

Actions are inaccessible via conventional routes defined by UseEndpoints , UseMvc, or


UseMvcWithDefaultRoute.
Automatic HTTP 400 responses
The [ApiController] attribute makes model validation errors automatically trigger an
HTTP 400 response. Consequently, the following code is unnecessary in an action
method:

C#

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

ASP.NET Core MVC uses the ModelStateInvalidFilter action filter to do the preceding
check.

Default BadRequest response


The default response type for an HTTP 400 response is ValidationProblemDetails. The
following response body is an example of the serialized type:

JSON

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7fb5e16a-4c8f23bbfc974667.",
"errors": {
"": [
"A non-empty request body is required."
]
}
}

The ValidationProblemDetails type:

Provides a machine-readable format for specifying errors in web API responses.


Complies with the RFC 7807 specification .

To make automatic and custom responses consistent, call the ValidationProblem method
instead of BadRequest. ValidationProblem returns a ValidationProblemDetails object as
well as the automatic response.

Log automatic 400 responses


To log automatic 400 responses, set the InvalidModelStateResponseFactory delegate
property to perform custom processing. By default, InvalidModelStateResponseFactory
uses ProblemDetailsFactory to create an instance of ValidationProblemDetails.

The following example shows how to retrieve an instance of ILogger<TCategoryName>


to log information about an automatic 400 response:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// To preserve the default behavior, capture the original delegate to
call later.
var builtInFactory = options.InvalidModelStateResponseFactory;

options.InvalidModelStateResponseFactory = context =>


{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();

// Perform logging here.


// ...

// Invoke the default behavior, which produces a


ValidationProblemDetails
// response.
// To produce a custom response, return a different
implementation of
// IActionResult instead.
return builtInFactory(context);
};
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Disable automatic 400 response


To disable the automatic 400 behavior, set the SuppressModelStateInvalidFilter property
to true . Add the following highlighted code:
C#

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
"https://httpstatuses.com/404";
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Binding source parameter inference


A binding source attribute defines the location at which an action parameter's value is
found. The following binding source attributes exist:

Attribute Binding source

[FromBody] Request body

[FromForm] Form data in the request body

[FromHeader] Request header

[FromQuery] Request query string parameter

[FromRoute] Route data from the current request

[FromServices] The request service injected as an action parameter

[AsParameters] Method parameters

2 Warning
Don't use [FromRoute] when values might contain %2f (that is / ). %2f won't be
unescaped to / . Use [FromQuery] if the value might contain %2f .

Without the [ApiController] attribute or binding source attributes like [FromQuery] , the
ASP.NET Core runtime attempts to use the complex object model binder. The complex
object model binder pulls data from value providers in a defined order.

In the following example, the [FromQuery] attribute indicates that the discontinuedOnly
parameter value is provided in the request URL's query string:

C#

[HttpGet]
public ActionResult<List<Product>> Get(
[FromQuery] bool discontinuedOnly = false)
{
List<Product> products = null;

if (discontinuedOnly)
{
products = _productsInMemoryStore.Where(p =>
p.IsDiscontinued).ToList();
}
else
{
products = _productsInMemoryStore;
}

return products;
}

The [ApiController] attribute applies inference rules for the default data sources of
action parameters. These rules save you from having to identify binding sources
manually by applying attributes to the action parameters. The binding source inference
rules behave as follows:

[FromServices] is inferred for complex type parameters registered in the DI

Container.
[FromBody] is inferred for complex type parameters not registered in the DI

Container. An exception to the [FromBody] inference rule is any complex, built-in


type with a special meaning, such as IFormCollection and CancellationToken. The
binding source inference code ignores those special types.
[FromForm] is inferred for action parameters of type IFormFile and
IFormFileCollection. It's not inferred for any simple or user-defined types.
[FromRoute] is inferred for any action parameter name matching a parameter in

the route template. When more than one route matches an action parameter, any
route value is considered [FromRoute] .
[FromQuery] is inferred for any other action parameters.

FromBody inference notes


[FromBody] isn't inferred for simple types such as string or int . Therefore, the

[FromBody] attribute should be used for simple types when that functionality is needed.

When an action has more than one parameter bound from the request body, an
exception is thrown. For example, all of the following action method signatures cause an
exception:

[FromBody] inferred on both because they're complex types.

C#

[HttpPost]
public IActionResult Action1(Product product, Order order)

[FromBody] attribute on one, inferred on the other because it's a complex type.

C#

[HttpPost]
public IActionResult Action2(Product product, [FromBody] Order order)

[FromBody] attribute on both.

C#

[HttpPost]
public IActionResult Action3([FromBody] Product product, [FromBody]
Order order)

FromServices inference notes


Parameter binding binds parameters through dependency injection when the type is
configured as a service. This means it's not 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 controller's action methods. It's not common to have a type in DI and as an
argument in an API controller action.

To disable [FromServices] inference for a single action parameter, apply the desired
binding source attribute to the parameter. For example, apply the [FromBody] attribute
to an action parameter that should be bound from the body of the request.

To disable [FromServices] inference globally, set DisableImplicitFromServicesParameters


to true :

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();

Types 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 mechanism to infer binding source of API Controller action parameters uses the
following rules:

A previously specified BindingInfo.BindingSource is never overwritten.


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

Disable inference rules


To disable binding source inference, set SuppressInferBindingSourcesForParameters to
true :

C#

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
"https://httpstatuses.com/404";
options.DisableImplicitFromServicesParameters = true;
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Multipart/form-data request inference


The [ApiController] attribute applies an inference rule for action parameters of type
IFormFile and IFormFileCollection. The multipart/form-data request content type is
inferred for these types.

To disable the default behavior, set the


SuppressConsumesConstraintForFormFileParameters property to true :

C#

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
"https://httpstatuses.com/404";
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Problem details for error status codes


MVC transforms an error result (a result with status code 400 or higher) to a result with
ProblemDetails. The ProblemDetails type is based on the RFC 7807 specification for
providing machine-readable error details in an HTTP response.

Consider the following code in a controller action:

C#

if (pet == null)
{
return NotFound();
}
The NotFound method produces an HTTP 404 status code with a ProblemDetails body.
For example:

JSON

{
type: "https://tools.ietf.org/html/rfc7231#section-6.5.4",
title: "Not Found",
status: 404,
traceId: "0HLHLV31KRN83:00000001"
}

Disable ProblemDetails response


The automatic creation of a ProblemDetails for error status codes is disabled when the
SuppressMapClientErrors property is set to true . Add the following code:

C#

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
"https://httpstatuses.com/404";
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
Define supported request content types with
the [Consumes] attribute
By default, an action supports all available request content types. For example, if an app
is configured to support both JSON and XML input formatters, an action supports
multiple content types, including application/json and application/xml .

The [Consumes] attribute allows an action to limit the supported request content types.
Apply the [Consumes] attribute to an action or controller, specifying one or more
content types:

C#

[HttpPost]
[Consumes("application/xml")]
public IActionResult CreateProduct(Product product)

In the preceding code, the CreateProduct action specifies the content type
application/xml . Requests routed to this action must specify a Content-Type header of

application/xml . Requests that don't specify a Content-Type header of application/xml


result in a 415 Unsupported Media Type response.

The [Consumes] attribute also allows an action to influence its selection based on an
incoming request's content type by applying a type constraint. Consider the following
example:

C#

[ApiController]
[Route("api/[controller]")]
public class ConsumesController : ControllerBase
{
[HttpPost]
[Consumes("application/json")]
public IActionResult PostJson(IEnumerable<int> values) =>
Ok(new { Consumes = "application/json", Values = values });

[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
public IActionResult PostForm([FromForm] IEnumerable<int> values) =>
Ok(new { Consumes = "application/x-www-form-urlencoded", Values =
values });
}
In the preceding code, ConsumesController is configured to handle requests sent to the
https://localhost:5001/api/Consumes URL. Both of the controller's actions, PostJson
and PostForm , handle POST requests with the same URL. Without the [Consumes]
attribute applying a type constraint, an ambiguous match exception is thrown.

The [Consumes] attribute is applied to both actions. The PostJson action handles
requests sent with a Content-Type header of application/json . The PostForm action
handles requests sent with a Content-Type header of application/x-www-form-
urlencoded .

Additional resources
View or download sample code . (How to download).
Controller action return types in ASP.NET Core web API
Handle errors in ASP.NET Core web APIs
Custom formatters in ASP.NET Core Web API
Format response data in ASP.NET Core Web API
ASP.NET Core web API documentation with Swagger / OpenAPI
Routing to controller actions in ASP.NET Core
Use port tunneling Visual Studio to debug web APIs
Create a web API with ASP.NET Core
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
ASP.NET Core web API documentation
with Swagger / OpenAPI
Article • 11/10/2022

By Christoph Nienaber and Rico Suter

Swagger (OpenAPI) is a language-agnostic specification for describing REST APIs. It


allows both computers and humans to understand the capabilities of a REST API without
direct access to the source code. Its main goals are to:

Minimize the amount of work needed to connect decoupled services.


Reduce the amount of time needed to accurately document a service.

The two main OpenAPI implementations for .NET are Swashbuckle and NSwag , see:

Getting Started with Swashbuckle


Getting Started with NSwag

OpenAPI vs. Swagger


The Swagger project was donated to the OpenAPI Initiative in 2015 and has since been
referred to as OpenAPI. Both names are used interchangeably. However, "OpenAPI"
refers to the specification. "Swagger" refers to the family of open-source and
commercial products from SmartBear that work with the OpenAPI Specification.
Subsequent open-source products, such as OpenAPIGenerator , also fall under the
Swagger family name, despite not being released by SmartBear.

In short:

OpenAPI is a specification.
Swagger is tooling that uses the OpenAPI specification. For example,
OpenAPIGenerator and SwaggerUI.

OpenAPI specification ( openapi.json )


The OpenAPI specification is a document that describes the capabilities of your API. The
document is based on the XML and attribute annotations within the controllers and
models. It's the core part of the OpenAPI flow and is used to drive tooling such as
SwaggerUI. By default, it's named openapi.json . Here's an example of an OpenAPI
specification, reduced for brevity:
JSON

{
"openapi": "3.0.1",
"info": {
"title": "API V1",
"version": "v1"
},
"paths": {
"/api/Todo": {
"get": {
"tags": [
"Todo"
],
"operationId": "ApiTodoGet",
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ToDoItem"
}
}
},
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ToDoItem"
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ToDoItem"
}
}
}
}
}
}
},
"post": {

}
},
"/api/Todo/{id}": {
"get": {

},
"put": {

},
"delete": {

}
}
},
"components": {
"schemas": {
"ToDoItem": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string",
"nullable": true
},
"isCompleted": {
"type": "boolean"
}
},
"additionalProperties": false
}
}
}
}

Swagger UI
Swagger UI offers a web-based UI that provides information about the service, using
the generated OpenAPI specification. Both Swashbuckle and NSwag include an
embedded version of Swagger UI, so that it can be hosted in your ASP.NET Core app
using a middleware registration call. The web UI looks like this:
Each public action method in your controllers can be tested from the UI. Select a
method name to expand the section. Add any necessary parameters, and select Try it
out!.
7 Note

The Swagger UI version used for the screenshots is version 2. For a version 3
example, see Petstore example .

Next steps
Get started with Swashbuckle
Get started with NSwag
Get started with Swashbuckle and
ASP.NET Core
Article • 03/10/2023

There are three main components to Swashbuckle:

Swashbuckle.AspNetCore.Swagger : a Swagger object model and middleware to


expose SwaggerDocument objects as JSON endpoints.

Swashbuckle.AspNetCore.SwaggerGen : a Swagger generator that builds


SwaggerDocument objects directly from your routes, controllers, and models. It's

typically combined with the Swagger endpoint middleware to automatically


expose Swagger JSON.

Swashbuckle.AspNetCore.SwaggerUI : an embedded version of the Swagger UI


tool. It interprets Swagger JSON to build a rich, customizable experience for
describing the web API functionality. It includes built-in test harnesses for the
public methods.

Package installation
Swashbuckle can be added with the following approaches:

Visual Studio

From the Package Manager Console window:

Go to View > Other Windows > Package Manager Console

Navigate to the directory in which the .csproj file exists

Execute the following command:

PowerShell

Install-Package Swashbuckle.AspNetCore -Version 6.2.3

From the Manage NuGet Packages dialog:


Right-click the project in Solution Explorer > Manage NuGet Packages
Set the Package source to "nuget.org"
Ensure the "Include prerelease" option is enabled
Enter "Swashbuckle.AspNetCore" in the search box
Select the latest "Swashbuckle.AspNetCore" package from the Browse tab
and click Install

Add and configure Swagger middleware


Add the Swagger generator to the services collection in Program.cs :

C#

builder.Services.AddControllers();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

The call to AddEndpointsApiExplorer shown in the preceding example is required only


for minimal APIs. For more information, see this StackOverflow post .

Enable the middleware for serving the generated JSON document and the Swagger UI,
also in Program.cs :

C#

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

The preceding code adds the Swagger middleware only if the current environment is set
to Development. The UseSwaggerUI method call enables the Static File Middleware.

Launch the app and navigate to https://localhost:<port>/swagger/v1/swagger.json .


The generated document describing the endpoints appears as shown in OpenAPI
specification (openapi.json).

The Swagger UI can be found at https://localhost:<port>/swagger . Explore the API via


Swagger UI and incorporate it in other programs.

 Tip
To serve the Swagger UI at the app's root ( https://localhost:<port>/ ), set the
RoutePrefix property to an empty string:

C#

app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
options.RoutePrefix = string.Empty;
});

If using directories with IIS or a reverse proxy, set the Swagger endpoint to a relative
path using the ./ prefix. For example, ./swagger/v1/swagger.json . Using
/swagger/v1/swagger.json instructs the app to look for the JSON file at the true root of

the URL (plus the route prefix, if used). For example, use https://localhost:
<port>/<route_prefix>/swagger/v1/swagger.json instead of https://localhost:

<port>/<virtual_directory>/<route_prefix>/swagger/v1/swagger.json .

7 Note

By default, Swashbuckle generates and exposes Swagger JSON in version 3.0 of the
specification—officially called the OpenAPI Specification. To support backwards
compatibility, you can opt into exposing JSON in the 2.0 format instead. This 2.0
format is important for integrations such as Microsoft Power Apps and Microsoft
Flow that currently support OpenAPI version 2.0. To opt into the 2.0 format, set the
SerializeAsV2 property in Program.cs :

C#

app.UseSwagger(options =>
{
options.SerializeAsV2 = true;
});

Customize and extend


Swagger provides options for documenting the object model and customizing the UI to
match your theme.

API info and description


The configuration action passed to the AddSwaggerGen method adds information such as
the author, license, and description.

In Program.cs , import the following namespace to use the OpenApiInfo class:

C#

using Microsoft.OpenApi.Models;

Using the OpenApiInfo class, modify the information displayed in the UI:

C#

builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "ToDo API",
Description = "An ASP.NET Core Web API for managing ToDo items",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Example Contact",
Url = new Uri("https://example.com/contact")
},
License = new OpenApiLicense
{
Name = "Example License",
Url = new Uri("https://example.com/license")
}
});
});

The Swagger UI displays the version's information:


XML comments
XML comments can be enabled with the following approaches:

Visual Studio

Right-click the project in Solution Explorer and select Edit


<project_name>.csproj .
Add GenerateDocumentationFile to the .csproj file:

XML

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

Enabling XML comments provides debug information for undocumented public types
and members. Undocumented types and members are indicated by the warning
message. For example, the following message indicates a violation of warning code
1591:

text

warning CS1591: Missing XML comment for publicly visible type or member
'TodoController'
To suppress warnings project-wide, define a semicolon-delimited list of warning codes
to ignore in the project file. Appending the warning codes to $(NoWarn); applies the C#
default values too.

XML

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

To suppress warnings only for specific members, enclose the code in #pragma warning
preprocessor directives. This approach is useful for code that shouldn't be exposed via
the API docs. In the following example, warning code CS1591 is ignored for the entire
TodoContext class. Enforcement of the warning code is restored at the close of the class
definition. Specify multiple warning codes with a comma-delimited list.

C#

namespace SwashbuckleSample.Models;

#pragma warning disable CS1591


public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options) :
base(options) { }

public DbSet<TodoItem> TodoItems => Set<TodoItem>();


}
#pragma warning restore CS1591

Configure Swagger to use the XML file that's generated with the preceding instructions.
For Linux or non-Windows operating systems, file names and paths can be case-
sensitive. For example, a TodoApi.XML file is valid on Windows but not CentOS.

C#

builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "ToDo API",
Description = "An ASP.NET Core Web API for managing ToDo items",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Example Contact",
Url = new Uri("https://example.com/contact")
},
License = new OpenApiLicense
{
Name = "Example License",
Url = new Uri("https://example.com/license")
}
});

// using System.Reflection;
var xmlFilename = $"
{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory,
xmlFilename));
});

In the preceding code, Reflection is used to build an XML file name matching that of the
web API project. The AppContext.BaseDirectory property is used to construct a path to
the XML file. Some Swagger features (for example, schemata of input parameters or
HTTP methods and response codes from the respective attributes) work without the use
of an XML documentation file. For most features, namely method summaries and the
descriptions of parameters and response codes, the use of an XML file is mandatory.

Adding triple-slash comments to an action enhances the Swagger UI by adding the


description to the section header. Add a <summary> element above the Delete action:

C#

/// <summary>
/// Deletes a specific TodoItem.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
var item = await _context.TodoItems.FindAsync(id);

if (item is null)
{
return NotFound();
}

_context.TodoItems.Remove(item);
await _context.SaveChangesAsync();

return NoContent();
}

The Swagger UI displays the inner text of the preceding code's <summary> element:
The UI is driven by the generated JSON schema:

JSON

"delete": {
"tags": [
"Todo"
],
"summary": "Deletes a specific TodoItem.",
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
},

Add a <remarks> element to the Create action method documentation. It supplements


information specified in the <summary> element and provides a more robust Swagger UI.
The <remarks> element content can consist of text, JSON, or XML.

C#

/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item #1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(TodoItem item)
{
_context.TodoItems.Add(item);
await _context.SaveChangesAsync();

return CreatedAtAction(nameof(Get), new { id = item.Id }, item);


}

Notice the UI enhancements with these additional comments:

Data annotations
Mark the model with attributes, found in the System.ComponentModel.DataAnnotations
namespace, to help drive the Swagger UI components.

Add the [Required] attribute to the Name property of the TodoItem class:

C#

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace SwashbuckleSample.Models;
public class TodoItem
{
public long Id { get; set; }

[Required]
public string Name { get; set; } = null!;

[DefaultValue(false)]
public bool IsComplete { get; set; }
}

The presence of this attribute changes the UI behavior and alters the underlying JSON
schema:

JSON

"schemas": {
"TodoItem": {
"required": [
"name"
],
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"isComplete": {
"type": "boolean",
"default": false
}
},
"additionalProperties": false
}
},

Add the [Produces("application/json")] attribute to the API controller. Its purpose is to


declare that the controller's actions support a response content type of application/json:

C#

[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class TodoController : ControllerBase
{
The Media type drop-down selects this content type as the default for the controller's
GET actions:

As the usage of data annotations in the web API increases, the UI and API help pages
become more descriptive and useful.

Describe response types


Developers consuming a web API are most concerned with what's returned—specifically
response types and error codes (if not standard). The response types and error codes
are denoted in the XML comments and data annotations.

The Create action returns an HTTP 201 status code on success. An HTTP 400 status
code is returned when the posted request body is null. Without proper documentation
in the Swagger UI, the consumer lacks knowledge of these expected outcomes. Fix that
problem by adding the highlighted lines in the following example:

C#

/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item #1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(TodoItem item)
{
_context.TodoItems.Add(item);
await _context.SaveChangesAsync();

return CreatedAtAction(nameof(Get), new { id = item.Id }, item);


}

The Swagger UI now clearly documents the expected HTTP response codes:

Conventions can be used as an alternative to explicitly decorating individual actions with


[ProducesResponseType] . For more information, see Use web API conventions.

To support the [ProducesResponseType] decoration, the


Swashbuckle.AspNetCore.Annotations package offers extensions to enable and enrich
the response, schema, and parameter metadata.

Customize the UI
The default UI is both functional and presentable. However, API documentation pages
should represent your brand or theme. Branding the Swashbuckle components requires
adding the resources to serve static files and building the folder structure to host those
files.

Enable Static File Middleware:


C#

app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapControllers();

To inject additional CSS stylesheets, add them to the project's wwwroot folder and
specify the relative path in the middleware options:

C#

app.UseSwaggerUI(options =>
{
options.InjectStylesheet("/swagger-ui/custom.css");
});

Additional resources
View or download sample code (how to download)
Swagger doesn't recognize comments or attributes on records
Improve the developer experience of an API with Swagger documentation
Get started with NSwag and ASP.NET
Core
Article • 01/09/2023

By Christoph Nienaber , Rico Suter , and Dave Brock

View or download sample code (how to download)

NSwag offers the following capabilities:

The ability to utilize the Swagger UI and Swagger generator.


Flexible code generation capabilities.

With NSwag, you don't need an existing API—you can use third-party APIs that
incorporate Swagger and generate a client implementation. NSwag allows you to
expedite the development cycle and easily adapt to API changes.

Register the NSwag middleware


Register the NSwag middleware to:

Generate the Swagger specification for the implemented web API.


Serve the Swagger UI to browse and test the web API.

To use the NSwag ASP.NET Core middleware, install the NSwag.AspNetCore NuGet
package. This package contains the middleware to generate and serve the Swagger
specification, Swagger UI (v2 and v3), and ReDoc UI .

Use one of the following approaches to install the NSwag NuGet package:

Visual Studio

From the Package Manager Console window:

Go to View > Other Windows > Package Manager Console

Navigate to the directory in which the TodoApi.csproj file exists

Execute the following command:

PowerShell
Install-Package NSwag.AspNetCore

From the Manage NuGet Packages dialog:


Right-click the project in Solution Explorer > Manage NuGet Packages
Set the Package source to "nuget.org"
Enter "NSwag.AspNetCore" in the search box
Select the "NSwag.AspNetCore" package from the Browse tab and click
Install

Add and configure Swagger middleware


Add and configure Swagger in your ASP.NET Core app by performing the following
steps:

In the Startup.ConfigureServices method, register the required Swagger services:

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();

// Register the Swagger services


services.AddSwaggerDocument();
}

In the Startup.Configure method, enable the middleware for serving the


generated Swagger specification and the Swagger UI:

C#

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles();

// Register the Swagger generator and the Swagger UI middlewares


app.UseOpenApi();
app.UseSwaggerUi3();

app.UseMvc();
}
Launch the app. Navigate to:
http://localhost:<port>/swagger to view the Swagger UI.
http://localhost:<port>/swagger/v1/swagger.json to view the Swagger

specification.

Code generation
You can take advantage of NSwag's code generation capabilities by choosing one of the
following options:

NSwagStudio : A Windows desktop app for generating API client code in C# or


TypeScript.
The NSwag.CodeGeneration.CSharp or NSwag.CodeGeneration.TypeScript
NuGet packages for code generation inside your project.
NSwag from the command line .
The NSwag.MSBuild NuGet package.
The Unchase OpenAPI (Swagger) Connected Service : A Visual Studio Connected
Service for generating API client code in C# or TypeScript. Also generates C#
controllers for OpenAPI services with NSwag.

Generate code with NSwagStudio


Install NSwagStudio by following the instructions at the NSwagStudio GitHub
repository . On the NSwag release page you can download an xcopy version
which can be started without installation and admin privileges.

Launch NSwagStudio and enter the swagger.json file URL in the Swagger
Specification URL text box. For example,
http://localhost:44354/swagger/v1/swagger.json .

Click the Create local Copy button to generate a JSON representation of your
Swagger specification.
In the Outputs area, click the CSharp Client checkbox. Depending on your project,
you can also choose TypeScript Client or CSharp Web API Controller. If you select
CSharp Web API Controller, a service specification rebuilds the service, serving as
a reverse generation.

Click Generate Outputs to produce a complete C# client implementation of the


TodoApi.NSwag project. To see the generated client code, click the CSharp Client
tab:

C#

//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v12.0.9.0 (NJsonSchema v9.13.10.0
(Newtonsoft.Json v11.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
namespace MyNamespace
{
#pragma warning disable

[System.CodeDom.Compiler.GeneratedCode("NSwag", "12.0.9.0 (NJsonSchema


v9.13.10.0 (Newtonsoft.Json v11.0.0.0))")]
public partial class TodoClient
{
private string _baseUrl = "https://localhost:44354";
private System.Net.Http.HttpClient _httpClient;
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings>
_settings;

public TodoClient(System.Net.Http.HttpClient httpClient)


{
_httpClient = httpClient;
_settings = new
System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
{
var settings = new Newtonsoft.Json.JsonSerializerSettings();
UpdateJsonSerializerSettings(settings);
return settings;
});
}

public string BaseUrl


{
get { return _baseUrl; }
set { _baseUrl = value; }
}

// code omitted for brevity

 Tip

The C# client code is generated based on selections in the Settings tab. Modify the
settings to perform tasks such as default namespace renaming and synchronous
method generation.

Copy the generated C# code into a file in the client project that will consume the
API.
Start consuming the web API:

C#

var todoClient = new TodoClient();

// Gets all to-dos from the API


var allTodos = await todoClient.GetAllAsync();
// Create a new TodoItem, and save it via the API.
var createdTodo = await todoClient.CreateAsync(new TodoItem());

// Get a single to-do by ID


var foundTodo = await todoClient.GetByIdAsync(1);

Customize API documentation


Swagger provides options for documenting the object model to ease consumption of
the web API.

API info and description


In the Startup.ConfigureServices method, a configuration action passed to the
AddSwaggerDocument method adds information such as the author, license, and

description:

C#

services.AddSwaggerDocument(config =>
{
config.PostProcess = document =>
{
document.Info.Version = "v1";
document.Info.Title = "ToDo API";
document.Info.Description = "A simple ASP.NET Core web API";
document.Info.TermsOfService = "None";
document.Info.Contact = new NSwag.OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = "https://twitter.com/spboyer"
};
document.Info.License = new NSwag.OpenApiLicense
{
Name = "Use under LICX",
Url = "https://example.com/license"
};
};
});

The Swagger UI displays the version's information:


XML comments
To enable XML comments, perform the following steps:

Visual Studio

Right-click the project in Solution Explorer and select Edit


<project_name>.csproj .

Manually add the highlighted lines to the .csproj file:

XML

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

Data annotations
Because NSwag uses Reflection, and the recommended return type for web API actions
is ActionResult<T>, it can only infer the return type defined by T . You can't
automatically infer other possible return types.
Consider the following example:

C#

[HttpPost]
public ActionResult<TodoItem> Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

return CreatedAtRoute("GetTodo", new { id = item.Id }, item);


}

The preceding action returns ActionResult<T> . Inside the action, it's returning
CreatedAtRoute. Since the controller has the [ApiController] attribute, a BadRequest
response is possible, too. For more information, see Automatic HTTP 400 responses. Use
data annotations to tell clients which HTTP status codes this action is known to return.
Mark the action with the following attributes:

C#

[ProducesResponseType(StatusCodes.Status201Created)] // Created
[ProducesResponseType(StatusCodes.Status400BadRequest)] // BadRequest

In ASP.NET Core 2.2 or later, you can use conventions instead of explicitly decorating
individual actions with [ProducesResponseType] . For more information, see Use web API
conventions.

The Swagger generator can now accurately describe this action, and generated clients
know what they receive when calling the endpoint. As a recommendation, mark all
actions with these attributes.

For guidelines on what HTTP responses your API actions should return, see RFC 9110:
HTTP Semantics (Section 9.3. Method Definitions) .
.NET OpenAPI tool command reference
and installation
Article • 06/03/2022

Microsoft.dotnet-openapi is a .NET Core Global Tool for managing OpenAPI


references within a project.

Installation
To install Microsoft.dotnet-openapi , run the following command:

.NET CLI

dotnet tool install -g Microsoft.dotnet-openapi

Add
Adding an OpenAPI reference using any of the commands on this page adds an
<OpenApiReference /> element similar to the following to the .csproj file:

XML

<OpenApiReference Include="openapi.json" />

The preceding reference is required for the app to call the generated client code.

Add File

Options

Short Long option Description Example


option

-p -- The project to operate on. dotnet openapi


updateProject add file --
updateProject
.\Ref.csproj
.\OpenAPI.json
Short Long option Description Example
option

-c --code- The code generator to apply to the reference. dotnet openapi


generator Options are NSwagCSharp and NSwagTypeScript . If add file
--code-generator is not specified the tooling .\OpenApi.json --
defaults to NSwagCSharp . code-generator

-h --help Show help information dotnet openapi


add file --help

Arguments

Argument Description Example

source-file The source to create a reference from. Must be an dotnet openapi add file
OpenAPI file. .\OpenAPI.json

Add URL

Options

Short Long option Description Example


option

-p -- The project to operate on. dotnet openapi add url --


updateProject updateProject .\Ref.csproj
https://contoso.com/openapi.json

-o --output-file Where to place the local copy of dotnet openapi add url
the OpenAPI file. https://contoso.com/openapi.json
--output-file myclient.json

-c --code- The code generator to apply to dotnet openapi add url


generator the reference. Options are https://contoso.com/openapi.json
NSwagCSharp and --code-generator
NSwagTypeScript .

-h --help Show help information dotnet openapi add url --help

Arguments

Argument Description Example


Argument Description Example

source- The source to create a reference from. dotnet openapi add url
URL Must be a URL. https://contoso.com/openapi.json

Remove
Removes the OpenAPI reference matching the given filename from the .csproj file.
When the OpenAPI reference is removed, clients won't be generated. Local .json and
.yaml files are deleted.

Options

Short Long option Description Example


option

-p -- The project to dotnet openapi remove --updateProject


updateProject operate on. .\Ref.csproj .\OpenAPI.json

-h --help Show help dotnet openapi remove --help


information

Arguments

Argument Description Example

source-file The source to remove the reference to. dotnet openapi remove .\OpenAPI.json

Refresh
Refreshes the local version of a file that was downloaded using the latest content from
the download URL.

Options

Short Long option Description Example


option

-p -- The project to dotnet openapi refresh --updateProject .\Ref.csproj


updateProject operate on. https://contoso.com/openapi.json
Short Long option Description Example
option

-h --help Show help dotnet openapi refresh --help


information

Arguments

Argument Description Example

source- The URL to refresh the reference dotnet openapi refresh


URL from. https://contoso.com/openapi.json
Controller action return types in
ASP.NET Core web API
Article • 06/20/2023

View or download sample code (how to download)

ASP.NET Core provides the following options for web API controller action return types:

Specific type
IActionResult
ActionResult<T>
HttpResults

This article explains when it's most appropriate to use each return type.

Specific type
The most basic action returns a primitive or complex data type, for example, string or a
custom object. Consider the following action, which returns a collection of custom
Product objects:

C#

[HttpGet]
public Task<List<Product>> Get() =>
_productContext.Products.OrderBy(p => p.Name).ToListAsync();

Without known conditions to safeguard against, returning a specific type could suffice.
The preceding action accepts no parameters, so parameter constraints validation isn't
needed.

When multiple return types are possible, it's common to mix an ActionResult return type
with the primitive or complex return type. Either IActionResult or ActionResult<T> are
necessary to accommodate this type of action. Several samples of multiple return types
are provided in this article.

Return IEnumerable<T> or IAsyncEnumerable<T>


See Return IEnumerable<T> or IAsyncEnumerable<T> for performance considerations.
ASP.NET Core buffers the result of actions that return IEnumerable<T> before writing
them to the response. Consider declaring the action signature's return type as
IAsyncEnumerable<T> to guarantee asynchronous iteration. Ultimately, the iteration
mode is based on the underlying concrete type being returned and the selected
formatter affects how the result is processed:

When using System.Text.Json formatter, MVC relies on the support that


System.Text.Json added to stream the result.
When using Newtonsoft.Json or with XML-based formatters the result is buffered.

Consider the following action, which returns sale-priced product records as


IEnumerable<Product> :

C#

[HttpGet("syncsale")]
public IEnumerable<Product> GetOnSaleProducts()
{
var products = _productContext.Products.OrderBy(p => p.Name).ToList();

foreach (var product in products)


{
if (product.IsOnSale)
{
yield return product;
}
}
}

The IAsyncEnumerable<Product> equivalent of the preceding action is:

C#

[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
var products = _productContext.Products.OrderBy(p =>
p.Name).AsAsyncEnumerable();

await foreach (var product in products)


{
if (product.IsOnSale)
{
yield return product;
}
}
}
IActionResult type
The IActionResult return type is appropriate when multiple ActionResult return types
are possible in an action. The ActionResult types represent various HTTP status codes.
Any non-abstract class deriving from ActionResult qualifies as a valid return type. Some
common return types in this category are BadRequestResult (400), NotFoundResult
(404), and OkObjectResult (200). Alternatively, convenience methods in the
ControllerBase class can be used to return ActionResult types from an action. For
example, return BadRequest(); is a shorthand form of return new BadRequestResult(); .

Because there are multiple return types and paths in this type of action, liberal use of
the [ProducesResponseType] attribute is necessary. This attribute produces more
descriptive response details for web API help pages generated by tools like Swagger.
[ProducesResponseType] indicates the known types and HTTP status codes to be

returned by the action.

Synchronous action
Consider the following synchronous action in which there are two possible return types:

C#

[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Product))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById_IActionResult(int id)
{
var product = _productContext.Products.Find(id);
return product == null ? NotFound() : Ok(product);
}

In the preceding action:

A 404 status code is returned when the product represented by id doesn't exist in
the underlying data store. The NotFound convenience method is invoked as
shorthand for return new NotFoundResult(); .
A 200 status code is returned with the Product object when the product does exist.
The Ok convenience method is invoked as shorthand for return new
OkObjectResult(product); .

Asynchronous action
Consider the following asynchronous action in which there are two possible return
types:

C#

[HttpPost()]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync_IActionResult(Product product)
{
if (product.Description.Contains("XYZ Widget"))
{
return BadRequest();
}

_productContext.Products.Add(product);
await _productContext.SaveChangesAsync();

return CreatedAtAction(nameof(GetById_IActionResult), new { id =


product.Id }, product);
}

In the preceding action:

A 400 status code is returned when the product description contains "XYZ Widget".
The BadRequest convenience method is invoked as shorthand for return new
BadRequestResult(); .

A 201 status code is generated by the CreatedAtAction convenience method when


a product is created. The following code is an alternative to calling
CreatedAtAction :

C#

return new CreatedAtActionResult(nameof(GetById),


"Products",
new { id = product.Id },
product);

In the preceding code path, the Product object is provided in the response body. A
Location response header containing the newly created product's URL is provided.

For example, the following model indicates that requests must include the Name and
Description properties. Failure to provide Name and Description in the request causes
model validation to fail.
C#

public class Product


{
public int Id { get; set; }

[Required]
public string Name { get; set; } = string.Empty;

[Required]
public string Description { get; set; } = string.Empty;

public bool IsOnSale { get; set; }


}

If the [ApiController] attribute is applied, model validation errors result in a 400 status
code. For more information, see Automatic HTTP 400 responses.

ActionResult vs IActionResult
The following section compares ActionResult to IActionResult

ActionResult<T> type
ASP.NET Core includes the ActionResult<T> return type for web API controller actions. It
enables returning a type deriving from ActionResult or return a specific type.
ActionResult<T> offers the following benefits over the IActionResult type:

The [ProducesResponseType] attribute's Type property can be excluded. For


example, [ProducesResponseType(200, Type = typeof(Product))] is simplified to
[ProducesResponseType(200)] . The action's expected return type is inferred from
the T in ActionResult<T> .
Implicit cast operators support the conversion of both T and ActionResult to
ActionResult<T> . T converts to ObjectResult, which means return new

ObjectResult(T); is simplified to return T; .

C# doesn't support implicit cast operators on interfaces. Consequently, conversion of


the interface to a concrete type is necessary to use ActionResult<T> . For example, use of
IEnumerable in the following example doesn't work:

C#

[HttpGet]
public ActionResult<IEnumerable<Product>> Get() =>
_repository.GetProducts();

One option to fix the preceding code is to return _repository.GetProducts().ToList(); .

Most actions have a specific return type. Unexpected conditions can occur during action
execution, in which case the specific type isn't returned. For example, an action's input
parameter may fail model validation. In such a case, it's common to return the
appropriate ActionResult type instead of the specific type.

Synchronous action
Consider a synchronous action in which there are two possible return types:

C#

[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Product> GetById_ActionResultOfT(int id)
{
var product = _productContext.Products.Find(id);
return product == null ? NotFound() : product;
}

In the preceding action:

A 404 status code is returned when the product doesn't exist in the database.
A 200 status code is returned with the corresponding Product object when the
product does exist.

Asynchronous action
Consider an asynchronous action in which there are two possible return types:

C#

[HttpPost()]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Product>> CreateAsync_ActionResultOfT(Product
product)
{
if (product.Description.Contains("XYZ Widget"))
{
return BadRequest();
}

_productContext.Products.Add(product);
await _productContext.SaveChangesAsync();

return CreatedAtAction(nameof(GetById_ActionResultOfT), new { id =


product.Id }, product);
}

In the preceding action:

A 400 status code (BadRequest) is returned by the ASP.NET Core runtime when:
The [ApiController] attribute has been applied and model validation fails.
The product description contains "XYZ Widget".
A 201 status code is generated by the CreatedAtAction method when a product is
created. In this code path, the Product object is provided in the response body. A
Location response header containing the newly created product's URL is provided.

HttpResults type
In addition to the MVC-specific built-in result types (IActionResult and ActionResult<T>),
ASP.NET Core includes the HttpResults types that can be used in both Minimal APIs and
Web API.

Different than the MVC-specific result types, the HttpResults :

Are a results implementation that is processed by a call to IResult.ExecuteAsync.

Does not leverage the configured Formatters. Not leveraging the configured
formatters means:
Some features like Content negotiation aren't available.
The produced Content-Type is decided by the HttpResults implementation.

The HttpResults can be useful when sharing code between Minimal APIs and Web API.

IResult type
The Microsoft.AspNetCore.Http.HttpResults namespace contains classes that implement
the IResult interface. The IResult interface defines a contract that represents the result
of an HTTP endpoint. The static Results class is used to create varying IResult objects
that represent different types of responses.

The Built-in results table shows the common result helpers.


Consider the following code:

C#

[HttpGet("{id}")]
[ProducesResponseType(typeof(Product), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IResult GetById(int id)
{
var product = _productContext.Products.Find(id);
return product == null ? Results.NotFound() : Results.Ok(product);
}

In the preceding action:

A 404 status code is returned when the product doesn't exist in the database.
A 200 status code is returned with the corresponding Product object when the
product does exist, generated by the Results.Ok<T>().

Consider the following code:

C#

[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(Product), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IResult> CreateAsync(Product product)
{
if (product.Description.Contains("XYZ Widget"))
{
return Results.BadRequest();
}

_productContext.Products.Add(product);
await _productContext.SaveChangesAsync();

var location = Url.Action(nameof(GetById), new { id = product.Id }) ??


$"/{product.Id}";
return Results.Created(location, product);
}

In the preceding action:

A 400 status code is returned when:


The [ApiController] attribute has been applied and model validation fails.
The product description contains "XYZ Widget".
A 201 status code is generated by the Results.Create method when a product is
created. In this code path, the Product object is provided in the response body. A
Location response header containing the newly created product's URL is provided.

Result<T> type
The static TypedResults class returns the concrete IResult implementation that allows
using IResult as return type. The usage of the concrete IResult implementation offers
the following benefit over the IResult type:

All the [ProducesResponseType] attribute's can be excluded, since the HttpResult


implementation contributes automatically to the endpoint metadata.

When multiple IResult return types are needed, returning Result<TResult1, TResultN>
is preferred over returning IResult . Returning Result<TResult1, TResultN> is preferred
because generic union types automatically retain the endpoint metadata.

The Results<TResult1, TResultN> union types implement implicit cast operators so that
the compiler can automatically convert the types specified in the generic arguments to
an instance of the union type. This has the added benefit of providing compile-time
checking that a route handler actually only returns the results that it declares it does.
Attempting to return a type that isn’t declared as one of the generic arguments to
Results<> results in a compilation error.

Consider the following code:

C#

[HttpGet("{id}")]
public Results<NotFound, Ok<Product>> GetById(int id)
{
var product = _productContext.Products.Find(id);
return product == null ? TypedResults.NotFound() :
TypedResults.Ok(product);
}

In the preceding action:

A 404 status code is returned when the product doesn't exist in the database.
A 200 status code is returned with the corresponding Product object when the
product does exist, generated by the TypedResults.Ok<T>.

C#
[HttpPost]
public async Task<Results<BadRequest, Created<Product>>> CreateAsync(Product
product)
{
if (product.Description.Contains("XYZ Widget"))
{
return TypedResults.BadRequest();
}

_productContext.Products.Add(product);
await _productContext.SaveChangesAsync();

var location = Url.Action(nameof(GetById), new { id = product.Id }) ??


$"/{product.Id}";
return TypedResults.Created(location, product);
}

In the preceding action:

A 400 status code is returned when:


The [ApiController] attribute was applied and model validation fails.
The product description contains "XYZ Widget".
A 201 status code is generated by the TypedResults.Create method when a
product is created. In this code path, the Product object is provided in the
response body. A Location response header containing the newly created
product's URL is provided.

Additional resources
Handle requests with controllers in ASP.NET Core MVC
Model validation in ASP.NET Core MVC
ASP.NET Core web API documentation with Swagger / OpenAPI
JsonPatch in ASP.NET Core web API
Article • 03/08/2023

This article explains how to handle JSON Patch requests in an ASP.NET Core web API.

Package installation
JSON Patch support in ASP.NET Core web API is based on Newtonsoft.Json and requires
the Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package. To enable JSON Patch
support:

Install the Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package.

Call AddNewtonsoftJson. For example:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.AddNewtonsoftJson();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

AddNewtonsoftJson replaces the default System.Text.Json -based input and output


formatters used for formatting all JSON content. This extension method is compatible
with the following MVC service registration methods:

AddRazorPages
AddControllersWithViews
AddControllers

JsonPatch requires setting the Content-Type header to application/json-patch+json .


Add support for JSON Patch when using
System.Text.Json
The System.Text.Json -based input formatter doesn't support JSON Patch. To add
support for JSON Patch using Newtonsoft.Json , while leaving the other input and output
formatters unchanged:

Install the Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package.

Update Program.cs :

C#

using JsonPatchSample;
using Microsoft.AspNetCore.Mvc.Formatters;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
options.InputFormatters.Insert(0,
MyJPIF.GetJsonPatchInputFormatter());
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Options;

namespace JsonPatchSample;

public static class MyJPIF


{
public static NewtonsoftJsonPatchInputFormatter
GetJsonPatchInputFormatter()
{
var builder = new ServiceCollection()
.AddLogging()
.AddMvc()
.AddNewtonsoftJson()
.Services.BuildServiceProvider();

return builder
.GetRequiredService<IOptions<MvcOptions>>()
.Value
.InputFormatters
.OfType<NewtonsoftJsonPatchInputFormatter>()
.First();
}
}

The preceding code creates an instance of NewtonsoftJsonPatchInputFormatter and


inserts it as the first entry in the MvcOptions.InputFormatters collection. This order of
registration ensures that:

NewtonsoftJsonPatchInputFormatter processes JSON Patch requests.


The existing System.Text.Json -based input and formatters process all other JSON
requests and responses.

Use the Newtonsoft.Json.JsonConvert.SerializeObject method to serialize a


JsonPatchDocument.

PATCH HTTP request method


The PUT and PATCH methods are used to update an existing resource. The difference
between them is that PUT replaces the entire resource, while PATCH specifies only the
changes.

JSON Patch
JSON Patch is a format for specifying updates to be applied to a resource. A JSON
Patch document has an array of operations. Each operation identifies a particular type of
change. Examples of such changes include adding an array element or replacing a
property value.

For example, the following JSON documents represent a resource, a JSON Patch
document for the resource, and the result of applying the Patch operations.

Resource example
JSON
{
"customerName": "John",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
}
]
}

JSON patch example


JSON

[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]

In the preceding JSON:

The op property indicates the type of operation.


The path property indicates the element to update.
The value property provides the new value.

Resource after patch


Here's the resource after applying the preceding JSON Patch document:

JSON

{
"customerName": "Barry",
"orders": [
{
"orderName": "Order0",
"orderType": null
},
{
"orderName": "Order1",
"orderType": null
},
{
"orderName": "Order2",
"orderType": null
}
]
}

The changes made by applying a JSON Patch document to a resource are atomic. If any
operation in the list fails, no operation in the list is applied.

Path syntax
The path property of an operation object has slashes between levels. For example,
"/address/zipCode" .

Zero-based indexes are used to specify array elements. The first element of the
addresses array would be at /addresses/0 . To add to the end of an array, use a hyphen

( - ) rather than an index number: /addresses/- .

Operations
The following table shows supported operations as defined in the JSON Patch
specification :

Operation Notes

add Add a property or array element. For existing property: set value.

remove Remove a property or array element.

replace Same as remove followed by add at same location.

move Same as remove from source followed by add to destination using value from
source.

copy Same as add to destination using value from source.

test Return success status code if value at path = provided value .


JSON Patch in ASP.NET Core
The ASP.NET Core implementation of JSON Patch is provided in the
Microsoft.AspNetCore.JsonPatch NuGet package.

Action method code


In an API controller, an action method for JSON Patch:

Is annotated with the HttpPatch attribute.


Accepts a JsonPatchDocument<TModel>, typically with [FromBody].
Calls ApplyTo(Object) on the patch document to apply the changes.

Here's an example:

C#

[HttpPatch]
public IActionResult JsonPatchWithModelState(
[FromBody] JsonPatchDocument<Customer> patchDoc)
{
if (patchDoc != null)
{
var customer = CreateCustomer();

patchDoc.ApplyTo(customer, ModelState);

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

return new ObjectResult(customer);


}
else
{
return BadRequest(ModelState);
}
}

This code from the sample app works with the following Customer model:

C#

namespace JsonPatchSample.Models;

public class Customer


{
public string? CustomerName { get; set; }
public List<Order>? Orders { get; set; }
}

C#

namespace JsonPatchSample.Models;

public class Order


{
public string OrderName { get; set; }
public string OrderType { get; set; }
}

The sample action method:

Constructs a Customer .
Applies the patch.
Returns the result in the body of the response.

In a real app, the code would retrieve the data from a store such as a database and
update the database after applying the patch.

Model state
The preceding action method example calls an overload of ApplyTo that takes model
state as one of its parameters. With this option, you can get error messages in
responses. The following example shows the body of a 400 Bad Request response for a
test operation:

JSON

{
"Customer": [
"The current value 'John' at path 'customerName' != test value 'Nancy'."
]
}

Dynamic objects
The following action method example shows how to apply a patch to a dynamic object:

C#
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
dynamic obj = new ExpandoObject();
patch.ApplyTo(obj);

return Ok(obj);
}

The add operation


If path points to an array element: inserts new element before the one specified by
path .

If path points to a property: sets the property value.


If path points to a nonexistent location:
If the resource to patch is a dynamic object: adds a property.
If the resource to patch is a static object: the request fails.

The following sample patch document sets the value of CustomerName and adds an
Order object to the end of the Orders array.

JSON

[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]

The remove operation


If path points to an array element: removes the element.
If path points to a property:
If resource to patch is a dynamic object: removes the property.
If resource to patch is a static object:
If the property is nullable: sets it to null.
If the property is non-nullable, sets it to default<T> .

The following sample patch document sets CustomerName to null and deletes Orders[0] :

JSON

[
{
"op": "remove",
"path": "/customerName"
},
{
"op": "remove",
"path": "/orders/0"
}
]

The replace operation


This operation is functionally the same as a remove followed by an add .

The following sample patch document sets the value of CustomerName and replaces
Orders[0] with a new Order object:

JSON

[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]

The move operation


If path points to an array element: copies from element to location of path
element, then runs a remove operation on the from element.
If path points to a property: copies value of from property to path property, then
runs a remove operation on the from property.
If path points to a nonexistent property:
If the resource to patch is a static object: the request fails.
If the resource to patch is a dynamic object: copies from property to location
indicated by path , then runs a remove operation on the from property.

The following sample patch document:

Copies the value of Orders[0].OrderName to CustomerName .


Sets Orders[0].OrderName to null.
Moves Orders[1] to before Orders[0] .

JSON

[
{
"op": "move",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "move",
"from": "/orders/1",
"path": "/orders/0"
}
]

The copy operation


This operation is functionally the same as a move operation without the final remove
step.

The following sample patch document:

Copies the value of Orders[0].OrderName to CustomerName .


Inserts a copy of Orders[1] before Orders[0] .

JSON

[
{
"op": "copy",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "copy",
"from": "/orders/1",
"path": "/orders/0"
}
]

The test operation


If the value at the location indicated by path is different from the value provided in
value , the request fails. In that case, the whole PATCH request fails even if all other

operations in the patch document would otherwise succeed.

The test operation is commonly used to prevent an update when there's a concurrency
conflict.

The following sample patch document has no effect if the initial value of CustomerName is
"John", because the test fails:

JSON

[
{
"op": "test",
"path": "/customerName",
"value": "Nancy"
},
{
"op": "add",
"path": "/customerName",
"value": "Barry"
}
]

Get the code


View or download sample code . (How to download).

To test the sample, run the app and send HTTP requests with the following settings:

URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
HTTP method: PATCH
Header: Content-Type: application/json-patch+json
Body: Copy and paste one of the JSON patch document samples from the JSON
project folder.

Additional resources
IETF RFC 5789 PATCH method specification
IETF RFC 6902 JSON Patch specification
IETF RFC 6901 JSON Pointer
JSON Patch documentation . Includes links to resources for creating JSON Patch
documents.
ASP.NET Core JSON Patch source code
Format response data in ASP.NET Core
Web API
Article • 04/11/2023

ASP.NET Core MVC supports formatting response data, using specified formats or in
response to a client's request.

Format-specific Action Results


Some action result types are specific to a particular format, such as JsonResult and
ContentResult. Actions can return results that always use a specified format, ignoring a
client's request for a different format. For example, returning JsonResult returns JSON-
formatted data and returning ContentResult returns plain-text-formatted string data.

An action isn't required to return any specific type. ASP.NET Core supports any object
return value. Results from actions that return objects that aren't IActionResult types are
serialized using the appropriate IOutputFormatter implementation. For more
information, see Controller action return types in ASP.NET Core web API.

By default, the built-in helper method ControllerBase.Ok returns JSON-formatted data:

C#

[HttpGet]
public IActionResult Get()
=> Ok(_todoItemStore.GetList());

The sample code returns a list of todo items. Using the F12 browser developer tools or
Postman with the previous code displays:

The response header containing content-type: application/json; charset=utf-8 .


The request headers. For example, the Accept header. The Accept header is
ignored by the preceding code.

To return plain text formatted data, use ContentResult and the Content helper:

C#

[HttpGet("Version")]
public ContentResult GetVersion()
=> Content("v1.0.0");
In the preceding code, the Content-Type returned is text/plain .

For actions with multiple return types, return IActionResult . For example, when
returning different HTTP status codes based on the result of the operation.

Content negotiation
Content negotiation occurs when the client specifies an Accept header . The default
format used by ASP.NET Core is JSON . Content negotiation is:

Implemented by ObjectResult.
Built into the status code-specific action results returned from the helper methods.
The action results helper methods are based on ObjectResult .

When a model type is returned, the return type is ObjectResult .

The following action method uses the Ok and NotFound helper methods:

C#

[HttpGet("{id:long}")]
public IActionResult GetById(long id)
{
var todo = _todoItemStore.GetById(id);

if (todo is null)
{
return NotFound();
}

return Ok(todo);
}

By default, ASP.NET Core supports the following media types:

application/json
text/json

text/plain

Tools such as Fiddler or Postman can set the Accept request header to specify the
return format. When the Accept header contains a type the server supports, that type is
returned. The next section shows how to add additional formatters.

Controller actions can return POCOs (Plain Old CLR Objects). When a POCO is returned,
the runtime automatically creates an ObjectResult that wraps the object. The client gets
the formatted serialized object. If the object being returned is null , a 204 No Content
response is returned.

The following example returns an object type:

C#

[HttpGet("{id:long}")]
public TodoItem? GetById(long id)
=> _todoItemStore.GetById(id);

In the preceding code, a request for a valid todo item returns a 200 OK response. A
request for an invalid todo item returns a 204 No Content response.

The Accept header


Content negotiation takes place when an Accept header appears in the request. When a
request contains an accept header, ASP.NET Core:

Enumerates the media types in the accept header in preference order.


Tries to find a formatter that can produce a response in one of the formats
specified.

If no formatter is found that can satisfy the client's request, ASP.NET Core:

Returns 406 Not Acceptable if MvcOptions.ReturnHttpNotAcceptable is set to


true , or -

Tries to find the first formatter that can produce a response.

If no formatter is configured for the requested format, the first formatter that can format
the object is used. If no Accept header appears in the request:

The first formatter that can handle the object is used to serialize the response.
There isn't any negotiation taking place. The server is determining what format to
return.

If the Accept header contains */* , the Header is ignored unless


RespectBrowserAcceptHeader is set to true on MvcOptions.

Browsers and content negotiation


Unlike typical API clients, web browsers supply Accept headers. Web browsers specify
many formats, including wildcards. By default, when the framework detects that the
request is coming from a browser:

The Accept header is ignored.


The content is returned in JSON, unless otherwise configured.

This approach provides a more consistent experience across browsers when consuming
APIs.

To configure an app to respect browser accept headers, set the


RespectBrowserAcceptHeader property to true :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
options.RespectBrowserAcceptHeader = true;
});

Configure formatters
Apps that need to support extra formats can add the appropriate NuGet packages and
configure support. There are separate formatters for input and output. Input formatters
are used by Model Binding. Output formatters are used to format responses. For
information on creating a custom formatter, see Custom Formatters.

Add XML format support


To configure XML formatters implemented using XmlSerializer, call
AddXmlSerializerFormatters:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.AddXmlSerializerFormatters();

When using the preceding code, controller methods return the appropriate format
based on the request's Accept header.

Configure System.Text.Json -based formatters


To configure features for the System.Text.Json -based formatters, use
Microsoft.AspNetCore.Mvc.JsonOptions.JsonSerializerOptions. The following highlighted
code configures PascalCase formatting instead of the default camelCase formatting:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});

To configure output serialization options for specific actions, use JsonResult. For
example:

C#

[HttpGet]
public IActionResult Get()
=> new JsonResult(
_todoItemStore.GetList(),
new JsonSerializerOptions { PropertyNamingPolicy = null });

Add Newtonsoft.Json -based JSON format support


The default JSON formatters use System.Text.Json . To use the Newtonsoft.Json -based
formatters, install the Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package and
configure it in Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.AddNewtonsoftJson();

In the preceding code, the call to AddNewtonsoftJson configures the following Web API,
MVC, and Razor Pages features to use Newtonsoft.Json :

Input and output formatters that read and write JSON


JsonResult
JSON Patch
IJsonHelper
TempData

Some features may not work well with System.Text.Json -based formatters and require a
reference to the Newtonsoft.Json -based formatters. Continue using the
Newtonsoft.Json -based formatters when the app:

Uses Newtonsoft.Json attributes. For example, [JsonProperty] or [JsonIgnore] .


Customizes the serialization settings.
Relies on features that Newtonsoft.Json provides.

To configure features for the Newtonsoft.Json -based formatters, use SerializerSettings:

C#

builder.Services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new
DefaultContractResolver();
});

To configure output serialization options for specific actions, use JsonResult . For
example:

C#

[HttpGet]
public IActionResult GetNewtonsoftJson()
=> new JsonResult(
_todoItemStore.GetList(),
new JsonSerializerSettings { ContractResolver = new
DefaultContractResolver() });

Format ProblemDetails and ValidationProblemDetails


responses
The following action method calls ControllerBase.Problem to create a ProblemDetails
response:

C#

[HttpGet("Error")]
public IActionResult GetError()
=> Problem("Something went wrong.");
A ProblemDetails response is always camelCase, even when the app sets the format to
PascalCase. ProblemDetails follows RFC 7807 , which specifies lowercase.

When the [ApiController] attribute is applied to a controller class, the controller creates
a ValidationProblemDetails response when Model Validation fails. This response includes
a dictionary that uses the model's property names as error keys, unchanged. For
example, the following model includes a single property that requires validation:

C#

public class SampleModel


{
[Range(1, 10)]
public int Value { get; set; }
}

By default, the ValidationProblemDetails response returned when the Value property is


invalid uses an error key of Value , as shown in the following example:

C#

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-00000000000000000000000000000000-000000000000000-00",
"errors": {
"Value": [
"The field Value must be between 1 and 10."
]
}
}

To format the property names used as error keys, add an implementation of


IMetadataDetailsProvider to the MvcOptions.ModelMetadataDetailsProviders collection.
The following example adds a System.Text.Json -based implementation,
SystemTextJsonValidationMetadataProvider , which formats property names as

camelCase by default:

C#

builder.Services.AddControllers();

builder.Services.Configure<MvcOptions>(options =>
{
options.ModelMetadataDetailsProviders.Add(
new SystemTextJsonValidationMetadataProvider());
});

SystemTextJsonValidationMetadataProvider also accepts an implementation of

JsonNamingPolicy in its constructor, which specifies a custom naming policy for


formatting property names.

To set a custom name for a property within a model, use the [JsonPropertyName]
attribute on the property:

C#

public class SampleModel


{
[Range(1, 10)]
[JsonPropertyName("sampleValue")]
public int Value { get; set; }
}

The ValidationProblemDetails response returned for the preceding model when the
Value property is invalid uses an error key of sampleValue , as shown in the following

example:

C#

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-00000000000000000000000000000000-000000000000000-00",
"errors": {
"sampleValue": [
"The field Value must be between 1 and 10."
]
}
}

To format the ValidationProblemDetails response using Newtonsoft.Json , use


NewtonsoftJsonValidationMetadataProvider :

C#

builder.Services.AddControllers()
.AddNewtonsoftJson();

builder.Services.Configure<MvcOptions>(options =>
{
options.ModelMetadataDetailsProviders.Add(
new NewtonsoftJsonValidationMetadataProvider());
});

By default, NewtonsoftJsonValidationMetadataProvider formats property names as


camelCase. NewtonsoftJsonValidationMetadataProvider also accepts an implementation
of NamingPolicy in its constructor, which specifies a custom naming policy for
formatting property names. To set a custom name for a property within a model, use the
[JsonProperty] attribute.

Specify a format
To restrict the response formats, apply the [Produces] filter. Like most Filters, [Produces]
can be applied at the action, controller, or global scope:

C#

[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class TodoItemsController : ControllerBase

The preceding [Produces] filter:

Forces all actions within the controller to return JSON-formatted responses for
POCOs (Plain Old CLR Objects) or ObjectResult and its derived types.
Return JSON-formatted responses even if other formatters are configured and the
client specifies a different format.

For more information, see Filters.

Special case formatters


Some special cases are implemented using built-in formatters. By default, string return
types are formatted as text/plain (text/html if requested via the Accept header). This
behavior can be deleted by removing the StringOutputFormatter. Formatters are
removed in Program.cs . Actions that have a model object return type return 204 No
Content when returning null . This behavior can be deleted by removing the

HttpNoContentOutputFormatter. The following code removes the


StringOutputFormatter and HttpNoContentOutputFormatter .

C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
// using Microsoft.AspNetCore.Mvc.Formatters;
options.OutputFormatters.RemoveType<StringOutputFormatter>();
options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
});

Without the StringOutputFormatter , the built-in JSON formatter formats string return
types. If the built-in JSON formatter is removed and an XML formatter is available, the
XML formatter formats string return types. Otherwise, string return types return 406
Not Acceptable .

Without the HttpNoContentOutputFormatter , null objects are formatted using the


configured formatter. For example:

The JSON formatter returns a response with a body of null .


The XML formatter returns an empty XML element with the attribute
xsi:nil="true" set.

Response format URL mappings


Clients can request a particular format as part of the URL, for example:

In the query string or part of the path.


By using a format-specific file extension such as .xml or .json.

The mapping from request path should be specified in the route the API is using. For
example:

C#

[ApiController]
[Route("api/[controller]")]
[FormatFilter]
public class TodoItemsController : ControllerBase
{
private readonly TodoItemStore _todoItemStore;

public TodoItemsController(TodoItemStore todoItemStore)


=> _todoItemStore = todoItemStore;

[HttpGet("{id:long}.{format?}")]
public TodoItem? GetById(long id)
=> _todoItemStore.GetById(id);
The preceding route allows the requested format to be specified using an optional file
extension. The [FormatFilter] attribute checks for the existence of the format value in the
RouteData and maps the response format to the appropriate formatter when the

response is created.

Route Formatter

/api/todoitems/5 The default output formatter

/api/todoitems/5.json The JSON formatter (if configured)

/api/todoitems/5.xml The XML formatter (if configured)

Polymorphic deserialization
Built-in features provide a limited range of polymorphic serialization but no support for
deserialization at all. Deserialization requires a custom converter. See Polymorphic
deserialization for a complete sample of polymorphic deserialization.

Additional resources
View or download sample code (how to download)
Custom formatters in ASP.NET Core Web
API
Article • 04/11/2023

ASP.NET Core MVC supports data exchange in Web APIs using input and output
formatters. Input formatters are used by Model Binding. Output formatters are used to
format responses.

The framework provides built-in input and output formatters for JSON and XML. It
provides a built-in output formatter for plain text, but doesn't provide an input
formatter for plain text.

This article shows how to add support for additional formats by creating custom
formatters. For an example of a custom plain text input formatter, see
TextPlainInputFormatter on GitHub.

View or download sample code (how to download)

When to use a custom formatter


Use a custom formatter to add support for a content type that isn't handled by the
built-in formatters.

Overview of how to create a custom formatter


To create a custom formatter:

For serializing data sent to the client, create an output formatter class.
For deserializing data received from the client, create an input formatter class.
Add instances of formatter classes to the InputFormatters and OutputFormatters
collections in MvcOptions.

Create a custom formatter


To create a formatter:

Derive the class from the appropriate base class. The sample app derives from
TextOutputFormatter and TextInputFormatter.
Specify supported media types and encodings in the constructor.
Override the CanReadType and CanWriteType methods.
Override the ReadRequestBodyAsync and WriteResponseBodyAsync methods.

The following code shows the VcardOutputFormatter class from the sample :

C#

public class VcardOutputFormatter : TextOutputFormatter


{
public VcardOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}

protected override bool CanWriteType(Type? type)


=> typeof(Contact).IsAssignableFrom(type)
|| typeof(IEnumerable<Contact>).IsAssignableFrom(type);

public override async Task WriteResponseBodyAsync(


OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var httpContext = context.HttpContext;
var serviceProvider = httpContext.RequestServices;

var logger =
serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
var buffer = new StringBuilder();

if (context.Object is IEnumerable<Contact> contacts)


{
foreach (var contact in contacts)
{
FormatVcard(buffer, contact, logger);
}
}
else
{
FormatVcard(buffer, (Contact)context.Object!, logger);
}

await httpContext.Response.WriteAsync(buffer.ToString(),
selectedEncoding);
}

private static void FormatVcard(


StringBuilder buffer, Contact contact, ILogger logger)
{
buffer.AppendLine("BEGIN:VCARD");
buffer.AppendLine("VERSION:2.1");
buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
buffer.AppendLine($"UID:{contact.Id}");
buffer.AppendLine("END:VCARD");

logger.LogInformation("Writing {FirstName} {LastName}",


contact.FirstName, contact.LastName);
}
}

Derive from the appropriate base class


For text media types (for example, vCard), derive from the TextInputFormatter or
TextOutputFormatter base class:

C#

public class VcardOutputFormatter : TextOutputFormatter

For binary types, derive from the InputFormatter or OutputFormatter base class.

Specify supported media types and encodings


In the constructor, specify supported media types and encodings by adding to the
SupportedMediaTypes and SupportedEncodings collections:

C#

public VcardOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}

A formatter class can not use constructor injection for its dependencies. For example,
ILogger<VcardOutputFormatter> can't be added as a parameter to the constructor. To

access services, use the context object that gets passed in to the methods. A code
example in this article and the sample show how to do this.

Override CanReadType and CanWriteType


Specify the type to deserialize into or serialize from by overriding the CanReadType or
CanWriteType methods. For example, to create vCard text from a Contact type and vice
versa:
C#

protected override bool CanWriteType(Type? type)


=> typeof(Contact).IsAssignableFrom(type)
|| typeof(IEnumerable<Contact>).IsAssignableFrom(type);

The CanWriteResult method


In some scenarios, CanWriteResult must be overridden rather than CanWriteType. Use
CanWriteResult if the following conditions are true:

The action method returns a model class.


There are derived classes that might be returned at runtime.
The derived class returned by the action must be known at runtime.

For example, suppose the action method:

Signature returns a Person type.


Can return a Student or Instructor type that derives from Person .

For the formatter to handle only Student objects, check the type of Object in the
context object provided to the CanWriteResult method. When the action method
returns IActionResult:

It's not necessary to use CanWriteResult .


The CanWriteType method receives the runtime type.

Override ReadRequestBodyAsync and


WriteResponseBodyAsync
Deserialization or serialization is performed in ReadRequestBodyAsync or
WriteResponseBodyAsync. The following example shows how to get services from the
dependency injection container. Services can't be obtained from constructor
parameters:

C#

public override async Task WriteResponseBodyAsync(


OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var httpContext = context.HttpContext;
var serviceProvider = httpContext.RequestServices;

var logger =
serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
var buffer = new StringBuilder();

if (context.Object is IEnumerable<Contact> contacts)


{
foreach (var contact in contacts)
{
FormatVcard(buffer, contact, logger);
}
}
else
{
FormatVcard(buffer, (Contact)context.Object!, logger);
}

await httpContext.Response.WriteAsync(buffer.ToString(),
selectedEncoding);
}

private static void FormatVcard(


StringBuilder buffer, Contact contact, ILogger logger)
{
buffer.AppendLine("BEGIN:VCARD");
buffer.AppendLine("VERSION:2.1");
buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
buffer.AppendLine($"UID:{contact.Id}");
buffer.AppendLine("END:VCARD");

logger.LogInformation("Writing {FirstName} {LastName}",


contact.FirstName, contact.LastName);
}

Configure MVC to use a custom formatter


To use a custom formatter, add an instance of the formatter class to the
MvcOptions.InputFormatters or MvcOptions.OutputFormatters collection:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
options.InputFormatters.Insert(0, new VcardInputFormatter());
options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});

Formatters are evaluated in the order they're inserted, where the first one takes
precedence.
The complete VcardInputFormatter class
The following code shows the VcardInputFormatter class from the sample :

C#

public class VcardInputFormatter : TextInputFormatter


{
public VcardInputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}

protected override bool CanReadType(Type type)


=> type == typeof(Contact);

public override async Task<InputFormatterResult> ReadRequestBodyAsync(


InputFormatterContext context, Encoding effectiveEncoding)
{
var httpContext = context.HttpContext;
var serviceProvider = httpContext.RequestServices;

var logger =
serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();

using var reader = new StreamReader(httpContext.Request.Body,


effectiveEncoding);
string? nameLine = null;

try
{
await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
await ReadLineAsync("VERSION:", reader, context, logger);

nameLine = await ReadLineAsync("N:", reader, context, logger);

var split = nameLine.Split(";".ToCharArray());


var contact = new Contact(FirstName: split[1], LastName:
split[0].Substring(2));

await ReadLineAsync("FN:", reader, context, logger);


await ReadLineAsync("END:VCARD", reader, context, logger);

logger.LogInformation("nameLine = {nameLine}", nameLine);

return await InputFormatterResult.SuccessAsync(contact);


}
catch
{
logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
return await InputFormatterResult.FailureAsync();
}
}

private static async Task<string> ReadLineAsync(


string expectedText, StreamReader reader, InputFormatterContext
context,
ILogger logger)
{
var line = await reader.ReadLineAsync();

if (line is null || !line.StartsWith(expectedText))


{
var errorMessage = $"Looked for '{expectedText}' and got
'{line}'";

context.ModelState.TryAddModelError(context.ModelName,
errorMessage);
logger.LogError(errorMessage);

throw new Exception(errorMessage);


}

return line;
}
}

Test the app


Run the sample app for this article , which implements basic vCard input and output
formatters. The app reads and writes vCards similar to the following format:

BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD

To see vCard output, run the app and send a Get request with Accept header
text/vcard to https://localhost:<port>/api/contacts .

To add a vCard to the in-memory collection of contacts:

Send a Post request to /api/contacts with a tool like Postman.


Set the Content-Type header to text/vcard .
Set vCard text in the body, formatted like the preceding example.
Additional resources
Format response data in ASP.NET Core Web API
Manage Protobuf references with dotnet-grpc
Use web API analyzers
Article • 04/11/2023

ASP.NET Core provides an MVC analyzers package intended for use with web API
projects. The analyzers work with controllers annotated with ApiControllerAttribute,
while building on web API conventions.

The analyzers package notifies you of any controller action that:

Returns an undeclared status code.


Returns an undeclared success result.
Documents a status code that isn't returned.
Includes an explicit model validation check.

Reference the analyzer package


The analyzers are included in the .NET Core SDK. To enable the analyzer in your project,
include the IncludeOpenAPIAnalyzers property in the project file:

XML

<PropertyGroup>
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>

Analyzers for web API conventions


OpenAPI documents contain status codes and response types that an action may return.
In ASP.NET Core MVC, attributes such as ProducesResponseTypeAttribute and
ProducesAttribute are used to document an action. ASP.NET Core web API
documentation with Swagger / OpenAPI goes into further detail on documenting your
web API.

One of the analyzers in the package inspects controllers annotated with


ApiControllerAttribute and identifies actions that don't entirely document their
responses. Consider the following example:

C#

// GET api/contacts/{guid}
[HttpGet("{id}", Name = "GetById")]
[ProducesResponseType(typeof(Contact), StatusCodes.Status200OK)]
public IActionResult Get(string id)
{
var contact = _contacts.Get(id);

if (contact == null)
{
return NotFound();
}

return Ok(contact);
}

The preceding action documents the HTTP 200 success return type but doesn't
document the HTTP 404 failure status code. The analyzer reports the missing
documentation for the HTTP 404 status code as a warning. An option to fix the problem
is provided.

Analyzers require Microsoft.NET.Sdk.Web


Analyzers don't work with library projects or projects referencing
Sdk="Microsoft.NET.Sdk" .

Additional resources
Use web API conventions
ASP.NET Core web API documentation with Swagger / OpenAPI
Create web APIs with ASP.NET Core
Use web API conventions
Article • 04/11/2023

Common API documentation can be extracted and applied to multiple actions,


controllers, or all controllers within an assembly. Web API conventions are a substitute
for decorating individual actions with [ProducesResponseType].

A convention allows you to:

Define the most common return types and status codes returned from a specific
type of action.
Identify actions that deviate from the defined standard.

Default conventions are available from


Microsoft.AspNetCore.Mvc.DefaultApiConventions. The conventions are demonstrated
with the ValuesController.cs added to an API project template:

C#

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace WebApp1.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}

// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}

// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}

// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}

// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}

Actions that follow the patterns in the ValuesController.cs work with the default
conventions. If the default conventions don't meet your needs, see Create web API
conventions.

At runtime, Microsoft.AspNetCore.Mvc.ApiExplorer understands conventions.


ApiExplorer is MVC's abstraction to communicate with OpenAPI (also known as
Swagger) document generators. Attributes from the applied convention are associated
with an action and are included in the action's OpenAPI documentation. API analyzers
also understand conventions. If your action is unconventional (for example, it returns a
status code that isn't documented by the applied convention), a warning encourages
you to document the status code.

View or download sample code (how to download)

Apply web API conventions


Conventions don't compose; each action may be associated with exactly one
convention. More specific conventions take precedence over less specific conventions.
The selection is non-deterministic when two or more conventions of the same priority
apply to an action. The following options exist to apply a convention to an action, from
the most specific to the least specific:

1. Microsoft.AspNetCore.Mvc.ApiConventionMethodAttribute — Applies to individual


actions and specifies the convention type and the convention method that applies.

In the following example, the default convention type's


Microsoft.AspNetCore.Mvc.DefaultApiConventions.Put convention method is
applied to the Update action:

C#
// PUT api/contactsconvention/{guid}
[HttpPut("{id}")]
[ApiConventionMethod(typeof(DefaultApiConventions),
nameof(DefaultApiConventions.Put))]
public IActionResult Update(string id, Contact contact)
{
var contactToUpdate = _contacts.Get(id);

if (contactToUpdate == null)
{
return NotFound();
}

_contacts.Update(contact);

return NoContent();
}

The Microsoft.AspNetCore.Mvc.DefaultApiConventions.Put convention method


applies the following attributes to the action:

C#

[ProducesDefaultResponseType]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]

For more information on [ProducesDefaultResponseType] , see Default Response .

2. Microsoft.AspNetCore.Mvc.ApiConventionTypeAttribute applied to a controller —


Applies the specified convention type to all actions on the controller. A convention
method is marked with hints that determine the actions to which the convention
method applies. For more information on hints, see Create web API conventions).

In the following example, the default set of conventions is applied to all actions in
ContactsConventionController:

C#

[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("api/[controller]")]
public class ContactsConventionController : ControllerBase
{
3. Microsoft.AspNetCore.Mvc.ApiConventionTypeAttribute applied to an assembly —
Applies the specified convention type to all controllers in the current assembly. As
a recommendation, apply assembly-level attributes in the Startup.cs file.

In the following example, the default set of conventions is applied to all controllers
in the assembly:

C#

[assembly: ApiConventionType(typeof(DefaultApiConventions))]
namespace ApiConventions
{
public class Startup
{

Create web API conventions


If the default API conventions don't meet your needs, create your own conventions. A
convention is:

A static type with methods.


Capable of defining response types and naming requirements on actions.

Response types
These methods are annotated with [ProducesResponseType] or
[ProducesDefaultResponseType] attributes. For example:

C#

public static class MyAppConventions


{
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public static void Find(int id)
{
}
}

If more specific metadata attributes are absent, applying this convention to an assembly
enforces that:

The convention method applies to any action named Find .


A parameter named id is present on the Find action.
Naming requirements
The [ApiConventionNameMatch] and [ApiConventionTypeMatch] attributes can be applied
to the convention method that determines the actions to which they apply. For example:

C#

[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Find(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
int id)
{ }

In the preceding example:

The
Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchBehavior.Prefix

option applied to the method indicates that the convention matches any action
prefixed with "Find". Examples of matching actions include Find , FindPet , and
FindById .

The
Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchBehavior.Suffix

applied to the parameter indicates that the convention matches methods with
exactly one parameter ending in the suffix identifier. Examples include parameters
such as id or petId . ApiConventionTypeMatch can be similarly applied to types to
constrain the parameter type. A params[] argument indicates remaining
parameters that don't need to be explicitly matched.

Additional resources
Video: Create metadata for NSwagClient
Video: Beginner's Series to: Web APIs
Use web API analyzers
ASP.NET Core web API documentation with Swagger / OpenAPI
Handle errors in ASP.NET Core web APIs
Article • 04/11/2023

This article describes how to handle errors and customize error handling with ASP.NET
Core web APIs.

Developer Exception Page


The Developer Exception Page shows detailed stack traces for server errors. It uses
DeveloperExceptionPageMiddleware to capture synchronous and asynchronous
exceptions from the HTTP pipeline and to generate error responses. For example,
consider the following controller action, which throws an exception:

C#

[HttpGet("Throw")]
public IActionResult Throw() =>
throw new Exception("Sample exception.");

When the Developer Exception Page detects an unhandled exception, it generates a


default plain-text response similar to the following example:

Console

HTTP/1.1 500 Internal Server Error


Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

System.Exception: Sample exception.


at HandleErrorsSample.Controllers.ErrorsController.Get() in ...
at lambda_method1(Closure , Object , Object[] )
at
Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResul
tExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor
executor, Object controller, Object[] arguments)
at
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeAction
MethodAsync()
at
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State&
next, Scope& scope, Object& state, Boolean& isCompleted)
at
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextAc
tionFilterAsync()
...

If the client requests an HTML-formatted response, the Developer Exception Page


generates a response similar to the following example:

Console

HTTP/1.1 500 Internal Server Error


Content-Type: text/html; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Internal Server Error</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
font-size: .813em;
color: #222;
background-color: #fff;
}

h1 {
color: #44525e;
margin: 15px 0 15px 0;
}

...

To request an HTML-formatted response, set the Accept HTTP request header to


text/html .

2 Warning

Don't enable the Developer Exception Page unless the app is running in the
Development environment. Don't share detailed exception information publicly
when the app runs in production. For more information on configuring
environments, see Use multiple environments in ASP.NET Core.

Exception handler
In non-development environments, use Exception Handling Middleware to produce an
error payload:

1. In Program.cs , call UseExceptionHandler to add the Exception Handling


Middleware:

C#

var app = builder.Build();

app.UseHttpsRedirection();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
}

app.UseAuthorization();

app.MapControllers();

app.Run();

2. Configure a controller action to respond to the /error route:

C#

[Route("/error")]
public IActionResult HandleError() =>
Problem();

The preceding HandleError action sends an RFC 7807 -compliant payload to the client.

2 Warning

Don't mark the error handler action method with HTTP method attributes, such as
HttpGet . Explicit verbs prevent some requests from reaching the action method.

For web APIs that use Swagger / OpenAPI, mark the error handler action with the
[ApiExplorerSettings] attribute and set its IgnoreApi property to true . This
attribute configuration excludes the error handler action from the app's OpenAPI
specification:

C#

[ApiExplorerSettings(IgnoreApi = true)]
Allow anonymous access to the method if unauthenticated users should see the
error.

Exception Handling Middleware can also be used in the Development environment to


produce a consistent payload format across all environments:

1. In Program.cs , register environment-specific Exception Handling Middleware


instances:

C#

if (app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error-development");
}
else
{
app.UseExceptionHandler("/error");
}

In the preceding code, the middleware is registered with:

A route of /error-development in the Development environment.


A route of /error in non-Development environments.

2. Add controller actions for both the Development and non-Development routes:

C#

[Route("/error-development")]
public IActionResult HandleErrorDevelopment(
[FromServices] IHostEnvironment hostEnvironment)
{
if (!hostEnvironment.IsDevelopment())
{
return NotFound();
}

var exceptionHandlerFeature =
HttpContext.Features.Get<IExceptionHandlerFeature>()!;

return Problem(
detail: exceptionHandlerFeature.Error.StackTrace,
title: exceptionHandlerFeature.Error.Message);
}

[Route("/error")]
public IActionResult HandleError() =>
Problem();
Use exceptions to modify the response
The contents of the response can be modified from outside of the controller using a
custom exception and an action filter:

1. Create a well-known exception type named HttpResponseException :

C#

public class HttpResponseException : Exception


{
public HttpResponseException(int statusCode, object? value = null)
=>
(StatusCode, Value) = (statusCode, value);

public int StatusCode { get; }

public object? Value { get; }


}

2. Create an action filter named HttpResponseExceptionFilter :

C#

public class HttpResponseExceptionFilter : IActionFilter,


IOrderedFilter
{
public int Order => int.MaxValue - 10;

public void OnActionExecuting(ActionExecutingContext context) { }

public void OnActionExecuted(ActionExecutedContext context)


{
if (context.Exception is HttpResponseException
httpResponseException)
{
context.Result = new
ObjectResult(httpResponseException.Value)
{
StatusCode = httpResponseException.StatusCode
};

context.ExceptionHandled = true;
}
}
}
The preceding filter specifies an Order of the maximum integer value minus 10.
This Order allows other filters to run at the end of the pipeline.

3. In Program.cs , add the action filter to the filters collection:

C#

builder.Services.AddControllers(options =>
{
options.Filters.Add<HttpResponseExceptionFilter>();
});

Validation failure error response


For web API controllers, MVC responds with a ValidationProblemDetails response type
when model validation fails. MVC uses the results of InvalidModelStateResponseFactory
to construct the error response for a validation failure. The following example replaces
the default factory with an implementation that also supports formatting responses as
XML, in Program.cs :

C#

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
new BadRequestObjectResult(context.ModelState)
{
ContentTypes =
{
// using static System.Net.Mime.MediaTypeNames;
Application.Json,
Application.Xml
}
};
})
.AddXmlSerializerFormatters();

Client error response


An error result is defined as a result with an HTTP status code of 400 or higher. For web
API controllers, MVC transforms an error result to produce a ProblemDetails.

The automatic creation of a ProblemDetails for error status codes is enabled by default,
but error responses can be configured in one of the following ways:
1. Use the problem details service
2. Implement ProblemDetailsFactory
3. Use ApiBehaviorOptions.ClientErrorMapping

Default problem details response


The following Program.cs file was generated by the web application templates for API
controllers:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Consider the following controller, which returns BadRequest when the input is invalid:

C#

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
// /api/values2/divide/1/2
[HttpGet("{Numerator}/{Denominator}")]
public IActionResult Divide(double Numerator, double Denominator)
{
if (Denominator == 0)
{
return BadRequest();
}

return Ok(Numerator / Denominator);


}

// /api/values2 /squareroot/4
[HttpGet("{radicand}")]
public IActionResult Squareroot(double radicand)
{
if (radicand < 0)
{
return BadRequest();
}

return Ok(Math.Sqrt(radicand));
}
}

A problem details response is generated with the previous code when any of the
following conditions apply:

The /api/values2/divide endpoint is called with a zero denominator.


The /api/values2/squareroot endpoint is called with a radicand less than zero.

The default problem details response body has the following type , title , and status
values:

JSON

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Bad Request",
"status": 400,
"traceId": "00-84c1fd4063c38d9f3900d06e56542d48-85d1d4-00"
}

Problem details service


ASP.NET Core supports creating Problem Details for HTTP APIs using the
IProblemDetailsService. For more information, see the Problem details service.

The following code configures the app to generate a problem details response for all
HTTP client and server error responses that don't have a body content yet:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

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

app.MapControllers();
app.Run();

Consider the API controller from the previous section, which returns BadRequest when
the input is invalid:

C#

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
// /api/values2/divide/1/2
[HttpGet("{Numerator}/{Denominator}")]
public IActionResult Divide(double Numerator, double Denominator)
{
if (Denominator == 0)
{
return BadRequest();
}

return Ok(Numerator / Denominator);


}

// /api/values2 /squareroot/4
[HttpGet("{radicand}")]
public IActionResult Squareroot(double radicand)
{
if (radicand < 0)
{
return BadRequest();
}

return Ok(Math.Sqrt(radicand));
}
}

A problem details response is generated with the previous code when any of the
following conditions apply:

An invalid input is supplied.


The URI has no matching endpoint.
An unhandled exception occurs.

The automatic creation of a ProblemDetails for error status codes is disabled when the
SuppressMapClientErrors property is set to true :
C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressMapClientErrors = true;
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Using the preceding code, when an API controller returns BadRequest , an HTTP 400
response status is returned with no response body. SuppressMapClientErrors prevents a
ProblemDetails response from being created, even when calling WriteAsync for an API
Controller endpoint. WriteAsync is explained later in this article.

The next section shows how to customize the problem details response body, using
CustomizeProblemDetails, to return a more helpful response. For more customization
options, see Customizing problem details.

Customize problem details with CustomizeProblemDetails

The following code uses ProblemDetailsOptions to set CustomizeProblemDetails:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = (context) =>
{

var mathErrorFeature = context.HttpContext.Features

.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
(string Detail, string Type) details =
mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError =>
("Divison by zero is not defined.",

"https://wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid
input.",

"https://wikipedia.org/wiki/Square_root")
};

context.ProblemDetails.Type = details.Type;
context.ProblemDetails.Title = "Bad Input";
context.ProblemDetails.Detail = details.Detail;
}
}
);

var app = builder.Build();

app.UseHttpsRedirection();

app.UseStatusCodePages();

app.UseAuthorization();

app.MapControllers();

app.Run();

The updated API controller:

C#

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
// /api/values/divide/1/2
[HttpGet("{Numerator}/{Denominator}")]
public IActionResult Divide(double Numerator, double Denominator)
{
if (Denominator == 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.DivisionByZeroError
};
HttpContext.Features.Set(errorType);
return BadRequest();
}
return Ok(Numerator / Denominator);
}

// /api/values/squareroot/4
[HttpGet("{radicand}")]
public IActionResult Squareroot(double radicand)
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
HttpContext.Features.Set(errorType);
return BadRequest();
}
return Ok(Math.Sqrt(radicand));
}

The following code contains the MathErrorFeature and MathErrorType , which are used
with the preceding sample:

C#

// Custom Http Request Feature


class MathErrorFeature
{
public MathErrorType MathError { get; set; }
}

// Custom math errors


enum MathErrorType
{
DivisionByZeroError,
NegativeRadicandError
}

A problem details response is generated with the previous code when any of the
following conditions apply:

The /divide endpoint is called with a zero denominator.


The /squareroot endpoint is called with a radicand less than zero.
The URI has no matching endpoint.

The problem details response body contains the following when either squareroot
endpoint is called with a radicand less than zero:
JSON

{
"type": "https://en.wikipedia.org/wiki/Square_root",
"title": "Bad Input",
"status": 400,
"detail": "Negative or complex numbers are not allowed."
}

View or download sample code

Implement ProblemDetailsFactory
MVC uses Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory to produce
all instances of ProblemDetails and ValidationProblemDetails. This factory is used for:

Client error responses


Validation failure error responses
ControllerBase.Problem and ControllerBase.ValidationProblem

To customize the problem details response, register a custom implementation of


ProblemDetailsFactory in Program.cs :

C#

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory,
SampleProblemDetailsFactory>();

Use ApiBehaviorOptions.ClientErrorMapping
Use the ClientErrorMapping property to configure the contents of the ProblemDetails
response. For example, the following code in Program.cs updates the Link property for
404 responses:

C#

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
"https://httpstatuses.com/404";
});
Additional resources
How to Use ModelState Validation in ASP.NET Core Web API
View or download sample code
Hellang.Middleware.ProblemDetails
Test web APIs with the HttpRepl
Article • 04/17/2023

The HTTP Read-Eval-Print Loop (REPL) is:

A lightweight, cross-platform command-line tool that's supported everywhere .NET


Core is supported.
Used for making HTTP requests to test ASP.NET Core web APIs (and non-ASP.NET
Core web APIs) and view their results.
Capable of testing web APIs hosted in any environment, including localhost and
Azure App Service.

The following HTTP verbs are supported:

DELETE
GET
HEAD
OPTIONS
PATCH
POST
PUT

To follow along, view or download the sample ASP.NET Core web API (how to
download).

Prerequisites
.NET Core 3.1 SDK

Installation
To install the HttpRepl, run the following command:

.NET CLI

dotnet tool install -g Microsoft.dotnet-httprepl

A .NET Core Global Tool is installed from the Microsoft.dotnet-httprepl NuGet


package.

On macOS, update the path:


Bash

export PATH="$HOME/.dotnet/tools:$PATH"

Usage
After successful installation of the tool, run the following command to start the
HttpRepl:

Console

httprepl

To view the available HttpRepl commands, run one of the following commands:

Console

httprepl -h

Console

httprepl --help

The following output is displayed:

Console

Usage:
httprepl [<BASE_ADDRESS>] [options]

Arguments:
<BASE_ADDRESS> - The initial base address for the REPL.

Options:
-h|--help - Show help information.

Once the REPL starts, these commands are valid:

Setup Commands:
Use these commands to configure the tool for your API server

connect Configures the directory structure and base address of the


api server
set header Sets or clears a header for all requests. e.g. `set header
content-type application/json`
HTTP Commands:
Use these commands to execute requests against your application.

GET get - Issues a GET request


POST post - Issues a POST request
PUT put - Issues a PUT request
DELETE delete - Issues a DELETE request
PATCH patch - Issues a PATCH request
HEAD head - Issues a HEAD request
OPTIONS options - Issues a OPTIONS request

Navigation Commands:
The REPL allows you to navigate your URL space and focus on specific APIs
that you are working on.

ls Show all endpoints for the current path


cd Append the given directory to the currently selected path, or
move up a path when using `cd ..`

Shell Commands:
Use these commands to interact with the REPL shell.

clear Removes all text from the shell


echo [on/off] Turns request echoing on or off, show the request that was
made when using request commands
exit Exit the shell

REPL Customization Commands:


Use these commands to customize the REPL behavior.

pref [get/set] Allows viewing or changing preferences, e.g. 'pref set


editor.command.default 'C:\\Program Files\\Microsoft VS Code\\Code.exe'`
run Runs the script at the given path. A script is a set of
commands that can be typed with one command per line
ui Displays the Swagger UI page, if available, in the default
browser

Use `help <COMMAND>` for more detail on an individual command. e.g. `help
get`.
For detailed tool info, see https://aka.ms/http-repl-doc.

The HttpRepl offers command completion. Pressing the Tab key iterates through the list
of commands that complete the characters or API endpoint that you typed. The
following sections outline the available CLI commands.

Connect to the web API


Connect to a web API by running the following command:

Console
httprepl <ROOT URI>

<ROOT URI> is the base URI for the web API. For example:

Console

httprepl https://localhost:5001

Alternatively, run the following command at any time while the HttpRepl is running:

Console

connect <ROOT URI>

For example:

Console

(Disconnected)> connect https://localhost:5001

Manually point to the OpenAPI description for the web


API
The connect command above will attempt to find the OpenAPI description
automatically. If for some reason it's unable to do so, you can specify the URI of the
OpenAPI description for the web API by using the --openapi option:

Console

connect <ROOT URI> --openapi <OPENAPI DESCRIPTION ADDRESS>

For example:

Console

(Disconnected)> connect https://localhost:5001 --openapi


/swagger/v1/swagger.json

Enable verbose output for details on OpenAPI description


searching, parsing, and validation
Specifying the --verbose option with the connect command will produce more details
when the tool searches for the OpenAPI description, parses, and validates it.

Console

connect <ROOT URI> --verbose

For example:

Console

(Disconnected)> connect https://localhost:5001 --verbose


Checking https://localhost:5001/swagger.json... 404 NotFound
Checking https://localhost:5001/swagger/v1/swagger.json... 404 NotFound
Checking https://localhost:5001/openapi.json... Found
Parsing... Successful (with warnings)
The field 'info' in 'document' object is REQUIRED [#/info]
The field 'paths' in 'document' object is REQUIRED [#/paths]

Navigate the web API

View available endpoints


To list the different endpoints (controllers) at the current path of the web API address,
run the ls or dir command:

Console

https://localhost:5001/> ls

The following output format is displayed:

Console

. []
Fruits [get|post]
People [get|post]

https://localhost:5001/>

The preceding output indicates that there are two controllers available: Fruits and
People . Both controllers support parameterless HTTP GET and POST operations.
Navigating into a specific controller reveals more detail. For example, the following
command's output shows the Fruits controller also supports HTTP GET, PUT, and
DELETE operations. Each of these operations expects an id parameter in the route:

Console

https://localhost:5001/fruits> ls
. [get|post]
.. []
{id} [get|put|delete]

https://localhost:5001/fruits>

Alternatively, run the ui command to open the web API's Swagger UI page in a
browser. For example:

Console

https://localhost:5001/> ui

Navigate to an endpoint
To navigate to a different endpoint on the web API, run the cd command:

Console

https://localhost:5001/> cd people

The path following the cd command is case insensitive. The following output format is
displayed:

Console

/people [get|post]

https://localhost:5001/people>

Customize the HttpRepl


The HttpRepl's default colors can be customized. Additionally, a default text editor can
be defined. The HttpRepl preferences are persisted across the current session and are
honored in future sessions. Once modified, the preferences are stored in the following
file:

Windows

%USERPROFILE%\.httpreplprefs

The .httpreplprefs file is loaded on startup and not monitored for changes at runtime.
Manual modifications to the file take effect only after restarting the tool.

View the settings


To view the available settings, run the pref get command. For example:

Console

https://localhost:5001/> pref get

The preceding command displays the available key-value pairs:

Console

colors.json=Green
colors.json.arrayBrace=BoldCyan
colors.json.comma=BoldYellow
colors.json.name=BoldMagenta
colors.json.nameSeparator=BoldWhite
colors.json.objectBrace=Cyan
colors.protocol=BoldGreen
colors.status=BoldYellow

Set color preferences


Response colorization is currently supported for JSON only. To customize the default
HttpRepl tool coloring, locate the key corresponding to the color to be changed. For
instructions on how to find the keys, see the View the settings section. For example,
change the colors.json key value from Green to White as follows:

Console

https://localhost:5001/people> pref set colors.json White


Only the allowed colors may be used. Subsequent HTTP requests display output with
the new coloring.

When specific color keys aren't set, more generic keys are considered. To demonstrate
this fallback behavior, consider the following example:

If colors.json.name doesn't have a value, colors.json.string is used.


If colors.json.string doesn't have a value, colors.json.literal is used.
If colors.json.literal doesn't have a value, colors.json is used.
If colors.json doesn't have a value, the command shell's default text color
( AllowedColors.None ) is used.

Set indentation size


Response indentation size customization is currently supported for JSON only. The
default size is two spaces. For example:

JSON

[
{
"id": 1,
"name": "Apple"
},
{
"id": 2,
"name": "Orange"
},
{
"id": 3,
"name": "Strawberry"
}
]

To change the default size, set the formatting.json.indentSize key. For example, to
always use four spaces:

Console

pref set formatting.json.indentSize 4

Subsequent responses honor the setting of four spaces:

JSON
[
{
"id": 1,
"name": "Apple"
},
{
"id": 2,
"name": "Orange"
},
{
"id": 3,
"name": "Strawberry"
}
]

Set the default text editor


By default, the HttpRepl has no text editor configured for use. To test web API methods
requiring an HTTP request body, a default text editor must be set. The HttpRepl tool
launches the configured text editor for the sole purpose of composing the request body.
Run the following command to set your preferred text editor as the default:

Console

pref set editor.command.default "<EXECUTABLE>"

In the preceding command, <EXECUTABLE> is the full path to the text editor's executable
file. For example, run the following command to set Visual Studio Code as the default
text editor:

Windows

Console

pref set editor.command.default "C:\Program Files\Microsoft VS


Code\Code.exe"

To launch the default text editor with specific CLI arguments, set the
editor.command.default.arguments key. For example, assume Visual Studio Code is the

default text editor and that you always want the HttpRepl to open Visual Studio Code in
a new session with extensions disabled. Run the following command:

Console
pref set editor.command.default.arguments "--disable-extensions --new-
window"

 Tip

If your default editor is Visual Studio Code, you'll usually want to pass the -w or --
wait argument to force Visual Studio Code to wait for you to close the file before
returning.

Set the OpenAPI Description search paths


By default, the HttpRepl has a set of relative paths that it uses to find the OpenAPI
description when executing the connect command without the --openapi option. These
relative paths are combined with the root and base paths specified in the connect
command. The default relative paths are:

swagger.json

swagger/v1/swagger.json

/swagger.json
/swagger/v1/swagger.json

openapi.json
/openapi.json

To use a different set of search paths in your environment, set the swagger.searchPaths
preference. The value must be a pipe-delimited list of relative paths. For example:

Console

pref set swagger.searchPaths


"swagger/v2/swagger.json|swagger/v3/swagger.json"

Instead of replacing the default list altogether, the list can also be modified by adding or
removing paths.

To add one or more search paths to the default list, set the swagger.addToSearchPaths
preference. The value must be a pipe-delimited list of relative paths. For example:

Console

pref set swagger.addToSearchPaths


"openapi/v2/openapi.json|openapi/v3/openapi.json"
To remove one or more search paths from the default list, set the
swagger.addToSearchPaths preference. The value must be a pipe-delimited list of relative

paths. For example:

Console

pref set swagger.removeFromSearchPaths "swagger.json|/swagger.json"

Test HTTP GET requests

Synopsis
Console

get <PARAMETER> [-F|--no-formatting] [-h|--header] [--response:body] [--


response:headers] [-s|--streaming]

Arguments
PARAMETER

The route parameter, if any, expected by the associated controller action method.

Options
The following options are available for the get command:

-F|--no-formatting

A flag whose presence suppresses HTTP response formatting.

-h|--header

Sets an HTTP request header. The following two value formats are supported:
{header}={value}

{header}:{value}

--response:body
Specifies a file to which the HTTP response body should be written. For example, -
-response:body "C:\response.json" . The file is created if it doesn't exist.

--response:headers

Specifies a file to which the HTTP response headers should be written. For
example, --response:headers "C:\response.txt" . The file is created if it doesn't
exist.

-s|--streaming

A flag whose presence enables streaming of the HTTP response.

Example
To issue an HTTP GET request:

1. Run the get command on an endpoint that supports it:

Console

https://localhost:5001/people> get

The preceding command displays the following output format:

Console

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 21 Jun 2019 03:38:45 GMT
Server: Kestrel
Transfer-Encoding: chunked

[
{
"id": 1,
"name": "Scott Hunter"
},
{
"id": 2,
"name": "Scott Hanselman"
},
{
"id": 3,
"name": "Scott Guthrie"
}
]
https://localhost:5001/people>

2. Retrieve a specific record by passing a parameter to the get command:

Console

https://localhost:5001/people> get 2

The preceding command displays the following output format:

Console

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 21 Jun 2019 06:17:57 GMT
Server: Kestrel
Transfer-Encoding: chunked

[
{
"id": 2,
"name": "Scott Hanselman"
}
]

https://localhost:5001/people>

Test HTTP POST requests

Synopsis
Console

post <PARAMETER> [-c|--content] [-f|--file] [-h|--header] [--no-body] [-F|--


no-formatting] [--response] [--response:body] [--response:headers] [-s|--
streaming]

Arguments
PARAMETER

The route parameter, if any, expected by the associated controller action method.
Options
-F|--no-formatting

A flag whose presence suppresses HTTP response formatting.

-h|--header

Sets an HTTP request header. The following two value formats are supported:
{header}={value}

{header}:{value}

--response:body

Specifies a file to which the HTTP response body should be written. For example, -
-response:body "C:\response.json" . The file is created if it doesn't exist.

--response:headers

Specifies a file to which the HTTP response headers should be written. For
example, --response:headers "C:\response.txt" . The file is created if it doesn't
exist.

-s|--streaming

A flag whose presence enables streaming of the HTTP response.

-c|--content

Provides an inline HTTP request body. For example, -c "


{\"id\":2,\"name\":\"Cherry\"}" .

-f|--file

Provides a path to a file containing the HTTP request body. For example, -f
"C:\request.json" .

--no-body

Indicates that no HTTP request body is needed.

Example
To issue an HTTP POST request:
1. Run the post command on an endpoint that supports it:

Console

https://localhost:5001/people> post -h Content-Type=application/json

In the preceding command, the Content-Type HTTP request header is set to


indicate a request body media type of JSON. The default text editor opens a .tmp
file with a JSON template representing the HTTP request body. For example:

JSON

{
"id": 0,
"name": ""
}

 Tip

To set the default text editor, see the Set the default text editor section.

2. Modify the JSON template to satisfy model validation requirements:

JSON

{
"id": 0,
"name": "Scott Addie"
}

3. Save the .tmp file, and close the text editor. The following output appears in the
command shell:

Console

HTTP/1.1 201 Created


Content-Type: application/json; charset=utf-8
Date: Thu, 27 Jun 2019 21:24:18 GMT
Location: https://localhost:5001/people/4
Server: Kestrel
Transfer-Encoding: chunked

{
"id": 4,
"name": "Scott Addie"
}
https://localhost:5001/people>

Test HTTP PUT requests

Synopsis
Console

put <PARAMETER> [-c|--content] [-f|--file] [-h|--header] [--no-body] [-F|--


no-formatting] [--response] [--response:body] [--response:headers] [-s|--
streaming]

Arguments
PARAMETER

The route parameter, if any, expected by the associated controller action method.

Options
-F|--no-formatting

A flag whose presence suppresses HTTP response formatting.

-h|--header

Sets an HTTP request header. The following two value formats are supported:
{header}={value}
{header}:{value}

--response:body

Specifies a file to which the HTTP response body should be written. For example, -
-response:body "C:\response.json" . The file is created if it doesn't exist.

--response:headers

Specifies a file to which the HTTP response headers should be written. For
example, --response:headers "C:\response.txt" . The file is created if it doesn't
exist.
-s|--streaming

A flag whose presence enables streaming of the HTTP response.

-c|--content

Provides an inline HTTP request body. For example, -c "


{\"id\":2,\"name\":\"Cherry\"}" .

-f|--file

Provides a path to a file containing the HTTP request body. For example, -f
"C:\request.json" .

--no-body

Indicates that no HTTP request body is needed.

Example
To issue an HTTP PUT request:

1. Optional: Run the get command to view the data before modifying it:

Console

https://localhost:5001/fruits> get
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 22 Jun 2019 00:07:32 GMT
Server: Kestrel
Transfer-Encoding: chunked

[
{
"id": 1,
"data": "Apple"
},
{
"id": 2,
"data": "Orange"
},
{
"id": 3,
"data": "Strawberry"
}
]
2. Run the put command on an endpoint that supports it:

Console

https://localhost:5001/fruits> put 2 -h Content-Type=application/json

In the preceding command, the Content-Type HTTP request header is set to


indicate a request body media type of JSON. The default text editor opens a .tmp
file with a JSON template representing the HTTP request body. For example:

JSON

{
"id": 0,
"name": ""
}

 Tip

To set the default text editor, see the Set the default text editor section.

3. Modify the JSON template to satisfy model validation requirements:

JSON

{
"id": 2,
"name": "Cherry"
}

4. Save the .tmp file, and close the text editor. The following output appears in the
command shell:

Console

[main 2019-06-28T17:27:01.805Z] update#setState idle


HTTP/1.1 204 No Content
Date: Fri, 28 Jun 2019 17:28:21 GMT
Server: Kestrel

5. Optional: Issue a get command to see the modifications. For example, if you typed
"Cherry" in the text editor, a get returns the following output:

Console
https://localhost:5001/fruits> get
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 22 Jun 2019 00:08:20 GMT
Server: Kestrel
Transfer-Encoding: chunked

[
{
"id": 1,
"data": "Apple"
},
{
"id": 2,
"data": "Cherry"
},
{
"id": 3,
"data": "Strawberry"
}
]

https://localhost:5001/fruits>

Test HTTP DELETE requests

Synopsis
Console

delete <PARAMETER> [-F|--no-formatting] [-h|--header] [--response] [--


response:body] [--response:headers] [-s|--streaming]

Arguments
PARAMETER

The route parameter, if any, expected by the associated controller action method.

Options
-F|--no-formatting

A flag whose presence suppresses HTTP response formatting.


-h|--header

Sets an HTTP request header. The following two value formats are supported:
{header}={value}

{header}:{value}

--response:body

Specifies a file to which the HTTP response body should be written. For example, -
-response:body "C:\response.json" . The file is created if it doesn't exist.

--response:headers

Specifies a file to which the HTTP response headers should be written. For
example, --response:headers "C:\response.txt" . The file is created if it doesn't
exist.

-s|--streaming

A flag whose presence enables streaming of the HTTP response.

Example
To issue an HTTP DELETE request:

1. Optional: Run the get command to view the data before modifying it:

Console

https://localhost:5001/fruits> get
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 22 Jun 2019 00:07:32 GMT
Server: Kestrel
Transfer-Encoding: chunked

[
{
"id": 1,
"data": "Apple"
},
{
"id": 2,
"data": "Orange"
},
{
"id": 3,
"data": "Strawberry"
}
]

2. Run the delete command on an endpoint that supports it:

Console

https://localhost:5001/fruits> delete 2

The preceding command displays the following output format:

Console

HTTP/1.1 204 No Content


Date: Fri, 28 Jun 2019 17:36:42 GMT
Server: Kestrel

3. Optional: Issue a get command to see the modifications. In this example, a get
returns the following output:

Console

https://localhost:5001/fruits> get
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 22 Jun 2019 00:16:30 GMT
Server: Kestrel
Transfer-Encoding: chunked

[
{
"id": 1,
"data": "Apple"
},
{
"id": 3,
"data": "Strawberry"
}
]

https://localhost:5001/fruits>

Test HTTP PATCH requests

Synopsis
Console

patch <PARAMETER> [-c|--content] [-f|--file] [-h|--header] [--no-body] [-F|-


-no-formatting] [--response] [--response:body] [--response:headers] [-s|--
streaming]

Arguments
PARAMETER

The route parameter, if any, expected by the associated controller action method.

Options
-F|--no-formatting

A flag whose presence suppresses HTTP response formatting.

-h|--header

Sets an HTTP request header. The following two value formats are supported:
{header}={value}

{header}:{value}

--response:body

Specifies a file to which the HTTP response body should be written. For example, -
-response:body "C:\response.json" . The file is created if it doesn't exist.

--response:headers

Specifies a file to which the HTTP response headers should be written. For
example, --response:headers "C:\response.txt" . The file is created if it doesn't
exist.

-s|--streaming

A flag whose presence enables streaming of the HTTP response.

-c|--content

Provides an inline HTTP request body. For example, -c "


{\"id\":2,\"name\":\"Cherry\"}" .
-f|--file

Provides a path to a file containing the HTTP request body. For example, -f
"C:\request.json" .

--no-body

Indicates that no HTTP request body is needed.

Test HTTP HEAD requests

Synopsis
Console

head <PARAMETER> [-F|--no-formatting] [-h|--header] [--response] [--


response:body] [--response:headers] [-s|--streaming]

Arguments
PARAMETER

The route parameter, if any, expected by the associated controller action method.

Options
-F|--no-formatting

A flag whose presence suppresses HTTP response formatting.

-h|--header

Sets an HTTP request header. The following two value formats are supported:
{header}={value}
{header}:{value}

--response:body

Specifies a file to which the HTTP response body should be written. For example, -
-response:body "C:\response.json" . The file is created if it doesn't exist.

--response:headers
Specifies a file to which the HTTP response headers should be written. For
example, --response:headers "C:\response.txt" . The file is created if it doesn't
exist.

-s|--streaming

A flag whose presence enables streaming of the HTTP response.

Test HTTP OPTIONS requests

Synopsis
Console

options <PARAMETER> [-F|--no-formatting] [-h|--header] [--response] [--


response:body] [--response:headers] [-s|--streaming]

Arguments
PARAMETER

The route parameter, if any, expected by the associated controller action method.

Options
-F|--no-formatting

A flag whose presence suppresses HTTP response formatting.

-h|--header

Sets an HTTP request header. The following two value formats are supported:
{header}={value}
{header}:{value}

--response:body

Specifies a file to which the HTTP response body should be written. For example, -
-response:body "C:\response.json" . The file is created if it doesn't exist.

--response:headers
Specifies a file to which the HTTP response headers should be written. For
example, --response:headers "C:\response.txt" . The file is created if it doesn't
exist.

-s|--streaming

A flag whose presence enables streaming of the HTTP response.

Set HTTP request headers


To set an HTTP request header, use one of the following approaches:

Set inline with the HTTP request. For example:

Console

https://localhost:5001/people> post -h Content-Type=application/json

With the preceding approach, each distinct HTTP request header requires its own
-h option.

Set before sending the HTTP request. For example:

Console

https://localhost:5001/people> set header Content-Type application/json

When setting the header before sending a request, the header remains set for the
duration of the command shell session. To clear the header, provide an empty
value. For example:

Console

https://localhost:5001/people> set header Content-Type

Test secured endpoints


The HttpRepl supports the testing of secured endpoints in the following ways:

Via the default credentials of the logged in user.


Through the use of HTTP request headers.
Default credentials
Consider a web API you're testing that's hosted in IIS and secured with Windows
authentication. You want the credentials of the user running the tool to flow across to
the HTTP endpoints being tested. To pass the default credentials of the logged in user:

1. Set the httpClient.useDefaultCredentials preference to true :

Console

pref set httpClient.useDefaultCredentials true

2. Exit and restart the tool before sending another request to the web API.

Default proxy credentials


Consider a scenario in which the web API you're testing is behind a proxy secured with
Windows authentication. You want the credentials of the user running the tool to flow to
the proxy. To pass the default credentials of the logged in user:

1. Set the httpClient.proxy.useDefaultCredentials preference to true :

Console

pref set httpClient.proxy.useDefaultCredentials true

2. Exit and restart the tool before sending another request to the web API.

HTTP request headers


Examples of supported authentication and authorization schemes include:

basic authentication
JWT bearer tokens
digest authentication

For example, you can send a bearer token to an endpoint with the following command:

Console

set header Authorization "bearer <TOKEN VALUE>"


To access an Azure-hosted endpoint or to use the Azure REST API, you need a bearer
token. Use the following steps to obtain a bearer token for your Azure subscription via
the Azure CLI. The HttpRepl sets the bearer token in an HTTP request header. A list of
Azure App Service Web Apps is retrieved.

1. Sign in to Azure:

Azure CLI

az login

2. Get your subscription ID with the following command:

Azure CLI

az account show --query id

3. Copy your subscription ID and run the following command:

Azure CLI

az account set --subscription "<SUBSCRIPTION ID>"

4. Get your bearer token with the following command:

Azure CLI

az account get-access-token --query accessToken

5. Connect to the Azure REST API via the HttpRepl:

Console

httprepl https://management.azure.com

6. Set the Authorization HTTP request header:

Console

https://management.azure.com/> set header Authorization "bearer <ACCESS


TOKEN>"

7. Navigate to the subscription:


Console

https://management.azure.com/> cd subscriptions/<SUBSCRIPTION ID>

8. Get a list of your subscription's Azure App Service Web Apps:

Console

https://management.azure.com/subscriptions/{SUBSCRIPTION ID}> get


providers/Microsoft.Web/sites?api-version=2016-08-01

The following response is displayed:

Console

HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Length: 35948
Content-Type: application/json; charset=utf-8
Date: Thu, 19 Sep 2019 23:04:03 GMT
Expires: -1
Pragma: no-cache
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
x-ms-correlation-request-id: <em>xxxxxxxx-xxxx-xxxx-xxxx-
xxxxxxxxxxxx</em>
x-ms-original-request-ids: <em>xxxxxxxx-xxxx-xxxx-xxxx-
xxxxxxxxxxxx;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</em>
x-ms-ratelimit-remaining-subscription-reads: 11999
x-ms-request-id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
x-ms-routing-request-id: WESTUS:xxxxxxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-
xxxx-xxxxxxxxxx
{
"value": [
<AZURE RESOURCES LIST>
]
}

Toggle HTTP request display


By default, display of the HTTP request being sent is suppressed. It's possible to change
the corresponding setting for the duration of the command shell session.

Enable request display


View the HTTP request being sent by running the echo on command. For example:
Console

https://localhost:5001/people> echo on
Request echoing is on

Subsequent HTTP requests in the current session display the request headers. For
example:

Console

https://localhost:5001/people> post

[main 2019-06-28T18:50:11.930Z] update#setState idle


Request to https://localhost:5001...

POST /people HTTP/1.1


Content-Length: 41
Content-Type: application/json
User-Agent: HTTP-REPL

{
"id": 0,
"name": "Scott Addie"
}

Response from https://localhost:5001...

HTTP/1.1 201 Created


Content-Type: application/json; charset=utf-8
Date: Fri, 28 Jun 2019 18:50:21 GMT
Location: https://localhost:5001/people/4
Server: Kestrel
Transfer-Encoding: chunked

{
"id": 4,
"name": "Scott Addie"
}

https://localhost:5001/people>

Disable request display


Suppress display of the HTTP request being sent by running the echo off command.
For example:

Console
https://localhost:5001/people> echo off
Request echoing is off

Run a script
If you frequently execute the same set of HttpRepl commands, consider storing them in
a text file. Commands in the file take the same form as commands executed manually
on the command line. The commands can be executed in a batched fashion using the
run command. For example:

1. Create a text file containing a set of newline-delimited commands. To illustrate,


consider a people-script.txt file containing the following commands:

text

set base https://localhost:5001


ls
cd People
ls
get 1

2. Execute the run command, passing in the text file's path. For example:

Console

https://localhost:5001/> run C:\http-repl-scripts\people-script.txt

The following output appears:

Console

https://localhost:5001/> set base https://localhost:5001


Using OpenAPI description at
https://localhost:5001/swagger/v1/swagger.json

https://localhost:5001/> ls
. []
Fruits [get|post]
People [get|post]

https://localhost:5001/> cd People
/People [get|post]

https://localhost:5001/People> ls
. [get|post]
.. []
{id} [get|put|delete]

https://localhost:5001/People> get 1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 12 Jul 2019 19:20:10 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
"id": 1,
"name": "Scott Hunter"
}

https://localhost:5001/People>

Clear the output


To remove all output written to the command shell by the HttpRepl tool, run the clear
or cls command. To illustrate, imagine the command shell contains the following
output:

Console

httprepl https://localhost:5001
(Disconnected)> set base "https://localhost:5001"
Using OpenAPI description at https://localhost:5001/swagger/v1/swagger.json

https://localhost:5001/> ls
. []
Fruits [get|post]
People [get|post]

https://localhost:5001/>

Run the following command to clear the output:

Console

https://localhost:5001/> clear

After running the preceding command, the command shell contains only the following
output:

Console
https://localhost:5001/>

Additional resources
REST API requests
HttpRepl GitHub repository
Configure Visual Studio to launch HttpRepl
Configure Visual Studio Code to launch HttpRepl
Configure Visual Studio for Mac to launch HttpRepl
HttpRepl telemetry
Article • 04/11/2023

The HttpRepl includes a telemetry feature that collects usage data. It's important that
the HttpRepl team understands how the tool is used so it can be improved.

How to opt out


The HttpRepl telemetry feature is enabled by default. To opt out of the telemetry
feature, set the DOTNET_HTTPREPL_TELEMETRY_OPTOUT environment variable to 1 or true .

Disclosure
The HttpRepl displays text similar to the following when you first run the tool. Text may
vary slightly depending on the version of the tool you're running. This "first run"
experience is how Microsoft notifies you about data collection.

Console

Telemetry
---------
The .NET tools collect usage data in order to help us improve your
experience. It is collected by Microsoft and shared with the community. You
can opt-out of telemetry by setting the DOTNET_HTTPREPL_TELEMETRY_OPTOUT
environment variable to '1' or 'true' using your favorite shell.

To suppress the "first run" experience text, set the


DOTNET_HTTPREPL_SKIP_FIRST_TIME_EXPERIENCE environment variable to 1 or true .

Data points
The telemetry feature doesn't:

Collect personal data, such as usernames, email addresses, or URLs.


Scan your HTTP requests or responses.

The data is sent securely to Microsoft servers and held under restricted access.

Protecting your privacy is important to us. If you suspect the telemetry feature is
collecting sensitive data or the data is being insecurely or inappropriately handled, take
one of the following actions:
File an issue in the dotnet/httprepl repository.
Send an email to dotnet@microsoft.com for investigation.

The telemetry feature collects the following data.

.NET Data
SDK
versions

>=5.0 Timestamp of invocation.

>=5.0 Three-octet IP address used to determine the geographical location.

>=5.0 Operating system and version.

>=5.0 Runtime ID (RID) the tool is running on.

>=5.0 Whether the tool is running in a container.

>=5.0 Hashed Media Access Control (MAC) address: a cryptographically (SHA256) hashed
and unique ID for a machine.

>=5.0 Kernel version.

>=5.0 HttpRepl version.

>=5.0 Whether the tool was started with help , run , or connect arguments. Actual argument
values aren't collected.

>=5.0 Command invoked (for example, get ) and whether it succeeded.

>=5.0 For the connect command, whether the root , base , or openapi arguments were
supplied. Actual argument values aren't collected.

>=5.0 For the pref command, whether a get or set was issued and which preference was
accessed. If not a well-known preference, the name is hashed. The value isn't collected.

>=5.0 For the set header command, the header name being set. If not a well-known header,
the name is hashed. The value isn't collected.

>=5.0 For the connect command, whether a special case for dotnet new webapi was used
and, whether it was bypassed via preference.

>=5.0 For all HTTP commands (for example, GET, POST, PUT), whether each of the options
was specified. The values of the options aren't collected.

Additional resources
.NET Core SDK telemetry
.NET Core CLI telemetry data
Minimal APIs overview
Article • 12/02/2022

Minimal APIs are a simplified approach for building fast HTTP APIs with ASP.NET Core.
You can build fully functioning REST endpoints with minimal code and configuration.
Skip traditional scaffolding and avoid unnecessary controllers by fluently declaring API
routes and actions. For example, the following code creates an API at the root of the
web app that returns the text, "Hello World!" .

C#

var app = WebApplication.Create(args);

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

app.Run();

Most APIs accept parameters as part of the route.

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is
{bookId}");

app.Run();

That's all it takes to get started, but it's not all that's available. Minimal APIs support the
configuration and customization needed to scale to multiple APIs, handle complex
routes, apply authorization rules, and control the content of API responses. A good
place to get started is Tutorial: Create a minimal API with ASP.NET Core.

Want to see some code examples?


For a full list of common scenarios with code examples, see Minimal APIs quick
reference.

Want to jump straight into your first project?


Build a minimal API app with our tutorial: Tutorial: Create a minimal API with ASP.NET
Core.
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.
Minimal APIs quick reference
Article • 06/22/2023

This document:

Provides a quick reference for minimal APIs.


Is intended for experienced developers. For an introduction, see Tutorial: Create a
minimal API with ASP.NET Core

The minimal APIs consist of:

WebApplication and WebApplicationBuilder


Route Handlers

WebApplication
The following code is generated by an ASP.NET Core template:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

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

app.Run();

The preceding code can be created via dotnet new web on the command line or
selecting the Empty Web template in Visual Studio.

The following code creates a WebApplication ( app ) without explicitly creating a


WebApplicationBuilder:

C#

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create initializes a new instance of the WebApplication class with


preconfigured defaults.
WebApplication automatically adds the following middleware depending on certain

conditions:

UseDeveloperExceptionPage is added first when the HostingEnvironment is


"Development" .
UseRouting is added second if user code didn't already call UseRouting and if there
are endpoints configured, for example app.MapGet .
UseEndpoints is added at the end of the middleware pipeline if any endpoints are
configured.
UseAuthentication is added immediately after UseRouting if user code didn't
already call UseAuthentication and if IAuthenticationSchemeProvider can be
detected in the service provider. IAuthenticationSchemeProvider is added by
default when using AddAuthentication, and services are detected using
IServiceProviderIsService.
UseAuthorization is added next if user code didn't already call UseAuthorization
and if IAuthorizationHandlerProvider can be detected in the service provider.
IAuthorizationHandlerProvider is added by default when using AddAuthorization,

and services are detected using IServiceProviderIsService .


User configured middleware and endpoints are added between UseRouting and
UseEndpoints .

The following code is effectively what the automatic middleware being added to the app
produces:

C#

if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

In some cases, the default middleware configuration isn't correct for the app and
requires modification. For example, UseCors should be called before UseAuthentication
and UseAuthorization. The app needs to call UseAuthentication and UseAuthorization if
UseCors is called:

C#

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

If middleware should be run before route matching occurs, UseRouting should be called
and the middleware should be placed before the call to UseRouting . UseEndpoints isn't
required in this case as it is automatically added as described previously:

C#

app.Use((context, next) =>


{
return next(context);
});

app.UseRouting();

// other middleware and endpoints

When adding a terminal middleware:

The middleware must be added after UseEndpoints .


The app needs to call UseRouting and UseEndpoints so that the terminal
middleware can be placed at the correct location.

C#

app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});

Terminal middleware is middleware that runs if no endpoint handles the request.

Working with ports


When a web app is created with Visual Studio or dotnet new , a
Properties/launchSettings.json file is created that specifies the ports the app responds

to. In the port setting samples that follow, running the app from Visual Studio returns an
error dialog Unable to connect to web server 'AppName' . Visual Studio returns an error
because it's expecting the port specified in Properties/launchSettings.json , but the
app is using the port specified by app.Run("http://localhost:3000") . Run the following
port changing samples from the command line.

The following sections set the port the app responds to.

C#

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

In the preceding code, the app responds to port 3000 .

Multiple ports
In the following code, the app responds to port 3000 and 4000 .

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

Set the port from the command line


The following command makes the app respond to port 7777 :

.NET CLI

dotnet run --urls="https://localhost:7777"

If the Kestrel endpoint is also configured in the appsettings.json file, the


appsettings.json file specified URL is used. For more information, see Kestrel endpoint

configuration

Read the port from environment

The following code reads the port from the environment:

C#

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

The preferred way to set the port from the environment is to use the ASPNETCORE_URLS
environment variable, which is shown in the following section.

Set the ports via the ASPNETCORE_URLS environment variable

The ASPNETCORE_URLS environment variable is available to set the port:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS supports multiple URLs:

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

For more information using the environment, see Use multiple environments in ASP.NET
Core
Listen on all interfaces
The following samples demonstrate listening on all interfaces

http://*:3000

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

Listen on all interfaces using ASPNETCORE_URLS


The preceding samples can use ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Specify HTTPS with development certificate


C#

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

For more information on the development certificate, see Trust the ASP.NET Core HTTPS
development certificate on Windows and macOS.

Specify HTTPS using a custom certificate


The following sections show how to specify the custom certificate using the
appsettings.json file and via configuration.

Specify the custom certificate with appsettings.json

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
Specify the custom certificate via configuration

C#

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key


builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Use the certificate APIs

C#

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath,
"cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath,
"key.pem");

httpsOptions.ServerCertificate =
X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();
Configuration
The following code reads from the configuration system:

C#

For more information, see Configuration in ASP.NET Core

Logging
The following code writes a message to the log on application startup:

C#

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

For more information, see Logging in .NET Core and ASP.NET Core

Access the Dependency Injection (DI) container


The following code shows how to get services from the DI container during application
startup:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())


{
var sampleService =
scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}

app.Run();

For more information, see Dependency injection in ASP.NET Core.

WebApplicationBuilder
This section contains sample code using WebApplicationBuilder.

Change the content root, application name, and


environment
The following code sets the content root, application name, and environment:

C#

var builder = WebApplication.CreateBuilder(new WebApplicationOptions


{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name:
{builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name:
{builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path:
{builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder


class with preconfigured defaults.

For more information, see ASP.NET Core fundamentals overview

Change the content root, app name, and environment by


environment variables or command line
The following table shows the environment variable and command-line argument used
to change the content root, app name, and environment:

feature Environment variable Command-line argument

Application name ASPNETCORE_APPLICATIONNAME --applicationName

Environment name ASPNETCORE_ENVIRONMENT --environment

Content root ASPNETCORE_CONTENTROOT --contentRoot

Add configuration providers


The following sample adds the INI configuration provider:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

For detailed information, see File configuration providers in Configuration in ASP.NET


Core.

Read configuration
By default the WebApplicationBuilder reads configuration from multiple sources,
including:

appSettings.json and appSettings.{environment}.json


Environment variables
The command line

Read the environment


-->

The following code reads HelloKey from configuration and displays the value at the /
endpoint. If the configuration value is null, "Hello" is assigned to message :

C#
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

For a complete list of configuration sources read, see Default configuration in


Configuration in ASP.NET Core

Add logging providers


C#

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.


builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

Add services
C#

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.


builder.Services.AddMemoryCache();

// Add a custom scoped service.


builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Customize the IHostBuilder


Existing extension methods on IHostBuilder can be accessed using the Host property:
C#

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.


builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout =
TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

Customize the IWebHostBuilder


Extension methods on IWebHostBuilder can be accessed using the
WebApplicationBuilder.WebHost property.

C#

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based


builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Change the web root


By default, the web root is relative to the content root in the wwwroot folder. Web root is
where the static files middleware looks for static files. Web root can be changed with
WebHostOptions , the command line, or with the UseWebRoot method:

C#

var builder = WebApplication.CreateBuilder(new WebApplicationOptions


{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();

app.Run();

Custom dependency injection (DI) container


The following example uses Autofac :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't


// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Add Middleware
Any existing ASP.NET Core middleware can be configured on the WebApplication :

C#

var app = WebApplication.Create(args);

// Setup the file server to serve static files.


app.UseFileServer();

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

app.Run();

For more information, see ASP.NET Core Middleware

Developer exception page


WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder
class with preconfigured defaults. The developer exception page is enabled in the
preconfigured defaults. When the following code is run in the development
environment, navigating to / renders a friendly page that shows the exception.
C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an
exception.");
});

app.Run();

ASP.NET Core Middleware


The following table lists some of the middleware frequently used with minimal APIs.

Middleware Description API

Authentication Provides authentication support. UseAuthentication

Authorization Provides authorization support. UseAuthorization

CORS Configures Cross-Origin Resource Sharing. UseCors

Exception Handler Globally handles exceptions thrown by the UseExceptionHandler


middleware pipeline.

Forwarded Headers Forwards proxied headers onto the current UseForwardedHeaders


request.

HTTPS Redirection Redirects all HTTP requests to HTTPS. UseHttpsRedirection

HTTP Strict Transport Security enhancement middleware that adds UseHsts


Security (HSTS) a special response header.

Request Logging Provides support for logging HTTP requests UseHttpLogging


and responses.

Request Timeouts Provides support for configuring request UseRequestTimeouts


timeouts, global default and per endpoint.

W3C Request Provides support for logging HTTP requests UseW3CLogging


Logging and responses in the W3C format .

Response Caching Provides support for caching responses. UseResponseCaching

Response Provides support for compressing responses. UseResponseCompression


Compression
Middleware Description API

Session Provides support for managing user sessions. UseSession

Static Files Provides support for serving static files and UseStaticFiles,
directory browsing. UseFileServer

WebSockets Enables the WebSockets protocol. UseWebSockets

The following sections cover request handling: routing, parameter binding, and
responses.

Routing
A configured WebApplication supports Map{Verb} and MapMethods where {Verb} is a
camel-cased HTTP method like Get , Post , Put or Delete :

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", () => "This is a GET");


app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },


() => "This is an options or head request ");

app.Run();

The Delegate arguments passed to these methods are called "route handlers".

Route Handlers
Route handlers are methods that execute when the route matches. Route handlers can
be a lambda expression, a local function, an instance method or a static method. Route
handlers can be synchronous or asynchronous.

Lambda expression
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

Local function
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Instance method
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}

Static method
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}

Endpoint defined outside of Program.cs


Minimal APIs don't have to be located in Program.cs .

Program.cs

C#

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

C#

namespace MinAPISeparateFile;

public static class TodoEndpoints


{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Get all todo items
await context.Response.WriteAsJsonAsync(new { Message = "All
todo items" });
});

app.MapGet("/{id}", async context =>


{
// Get one todo item
await context.Response.WriteAsJsonAsync(new { Message = "One
todo item" });
});
}
}

See also Route groups later in this article.

Named endpoints and link generation


Endpoints can be given names in order to generate URLs to the endpoint. Using a
named endpoint avoids having to hard code paths in an app:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")


.WithName("hi");

app.MapGet("/", (LinkGenerator linker) =>


$"The link to the hello route is {linker.GetPathByName("hi", values:
null)}");

app.Run();

The preceding code displays The link to the hello endpoint is /hello from the /
endpoint.

NOTE: Endpoint names are case sensitive.

Endpoint names:

Must be globally unique.


Are used as the OpenAPI operation id when OpenAPI support is enabled. For more
information, see OpenAPI.

Route Parameters
Route parameters can be captured as part of the route pattern definition:
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is
{bookId}");

app.Run();

The preceding code returns The user id is 3 and book id is 7 from the URI
/users/3/books/7 .

The route handler can declare the parameters to capture. When a request is made a
route with parameters declared to capture, the parameters are parsed and passed to the
handler. This makes it easy to capture the values in a type safe way. In the preceding
code, userId and bookId are both int .

In the preceding code, if either route value cannot be converted to an int , an exception
is thrown. The GET request /users/hello/books/3 throws the following exception:

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Wildcard and catch all routes


The following catch all route returns Routing to hello from the `/posts/hello' endpoint:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Route constraints
Route constraints constrain the matching behavior of a route.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t =>
t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post
{slug}");

app.Run();

The following table demonstrates the preceding route templates and their behavior:

Route Template Example Matching URI

/todos/{id:int} /todos/1

/todos/{text} /todos/something

/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

For more information, see Route constraint reference in Routing in ASP.NET Core.

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

Parameter binding
Parameter binding is the process of converting request data into strongly typed
parameters that are expressed by route handlers. A binding source determines where
parameters are bound from. Binding sources can be explicit or inferred based on HTTP
method and parameter type.

Supported binding sources:

Route values
Query string
Header
Body (as JSON)
Services provided by dependency injection
Custom

Binding from form values is not natively supported in .NET 6 and 7.

The following GET route handler uses some of these parameter binding sources:
C#

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,


int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string
customHeader,
Service service) => { });

class Service { }

The following table shows the relationship between the parameters used in the
preceding example and the associated binding sources.

Parameter Binding Source

id route value

page query string

customHeader header

service Provided by dependency injection

The HTTP methods GET , HEAD , OPTIONS , and DELETE don't implicitly bind from body. To
bind from body (as JSON) for these HTTP methods, bind explicitly with [FromBody] or
read from the HttpRequest.

The following example POST route handler uses a binding source of body (as JSON) for
the person parameter:

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);


The parameters in the preceding examples are all bound from request data
automatically. To demonstrate the convenience that parameter binding provides, the
following route handlers show how to read request data directly from the request:

C#

app.MapGet("/{id}", (HttpRequest request) =>


{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];

// ...
});

app.MapPost("/", async (HttpRequest request) =>


{
var person = await request.ReadFromJsonAsync<Person>();

// ...
});

Explicit Parameter Binding


Attributes can be used to explicitly declare where parameters are bound from.

C#

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", ([FromRoute] int id,


[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});

class Service { }

record Person(string Name, int Age);

Parameter Binding Source


Parameter Binding Source

id route value with the name id

page query string with the name "p"

service Provided by dependency injection

contentType header with the name "Content-Type"

7 Note

Binding from form values is not natively supported in .NET 6 and 7.

Parameter binding with dependency injection


Parameter binding for minimal APIs binds parameters through dependency injection
when the type is configured as a service. It's not necessary to explicitly apply the
[FromServices] attribute to a parameter. In the following code, both actions return the
time:

C#

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);


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

var app = builder.Build();

app.MapGet("/", ( IDateTime dateTime) => dateTime.Now);


app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

Optional parameters
Parameters declared in route handlers are treated as required:

If a request matches the route, the route handler only runs if all required
parameters are provided in the request.
Failure to provide all required parameters results in an error.

C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page


{pageNumber}");

app.Run();

URI result

/products? 3 returned
pageNumber=3

/products BadHttpRequestException : Required parameter "int pageNumber" was not


provided from query string.

/products/1 HTTP 404 error, no matching route

To make pageNumber optional, define the type as optional or provide a default value:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber


?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();

URI result

/products?pageNumber=3 3 returned

/products 1 returned

/products2 1 returned

The preceding nullable and default value applies to all sources:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();
app.MapPost("/products", (Product? product) => { });

app.Run();

The preceding code calls the method with a null product if no request body is sent.

NOTE: If invalid data is provided and the parameter is nullable, the route handler is not
run.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber


?? 1}");

app.Run();

URI result

/products? 3 returned
pageNumber=3

/products 1 returned

/products? BadHttpRequestException : Failed to bind parameter "Nullable<int>


pageNumber=two pageNumber" from "two".

/products/two HTTP 404 error, no matching route

See the Binding Failures section for more information.

Special types
The following types are bound without explicit attributes:

HttpContext: The context which holds all the information about the current HTTP
request or response:

C#

app.MapGet("/", (HttpContext context) =>


context.Response.WriteAsync("Hello World"));
HttpRequest and HttpResponse: The HTTP request and HTTP response:

C#

app.MapGet("/", (HttpRequest request, HttpResponse response) =>


response.WriteAsync($"Hello World {request.Query["name"]}"));

CancellationToken: The cancellation token associated with the current HTTP


request:

C#

app.MapGet("/", async (CancellationToken cancellationToken) =>


await MakeLongRunningRequestAsync(cancellationToken));

ClaimsPrincipal: The user associated with the request, bound from


HttpContext.User:

C#

app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);

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.

The following code implements a background queue:

C#

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService


{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken


stoppingToken)
{
await foreach (var dataStream in
_queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>
(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}

class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}

The following code binds the request body to a Stream :

C#

app.MapPost("/register", async (HttpRequest req, Stream body,


Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}

// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the
maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked
request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

var buffer = new byte[readSize];

// Read at least that many bytes from the body.


var read = await body.ReadAtLeastAsync(buffer, readSize,
throwOnEndOfStream: false);

// We read more than the max, so this is a bad request.


if (read > maxMessageSize)
{
return Results.BadRequest();
}

// Attempt to send the buffer to the background queue.


if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}

// We couldn't accept the message since we're overloaded.


return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

The following code shows the complete Program.cs file:

C#

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);


// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of
85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions


var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.


builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>
(maxQueueSize));

// Create a background queue service.


builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header


'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23,
"Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header
"Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\":
23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}

// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the
maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked
request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

var buffer = new byte[readSize];

// Read at least that many bytes from the body.


var read = await body.ReadAtLeastAsync(buffer, readSize,
throwOnEndOfStream: false);

// We read more than the max, so this is a bad request.


if (read > maxMessageSize)
{
return Results.BadRequest();
}

// Attempt to send the buffer to the background queue.


if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}

// We couldn't accept the message since we're overloaded.


return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();

When reading data, the Stream is the same object as HttpRequest.Body .


The request body isn't buffered by default. After the body is read, it's not
rewindable. The stream can't be read multiple times.
The Stream and PipeReader aren't usable outside of the minimal action handler as
the underlying buffers will be disposed or reused.
File uploads using 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.

Bind arrays and string values from headers and query


strings
The following code demonstrates binding query strings to an array of primitive types,
string arrays, and StringValues:

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. The following code binds to a string array and
returns all the items with the specified tags:

C#

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});

The following code shows the model and the required TryParse implementation:

C#

public class Todo


{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }

// This is an owned entity.


public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";

public static bool TryParse(string? name, out Tag tag)


{
if (name is null)
{
tag = default!;
return false;
}

tag = new Tag { Name = name };


return true;
}
}

The following code binds to an int array:

C#

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});

To test the preceding code, add the following endpoint to populate the database with
Todo items:

C#

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();

return Results.Ok(todos);
});

Use a tool like Postman to pass the following data to the previous endpoint:

C#

[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]

The following code binds to the header key X-Todo-Id and returns the Todo items with
matching Id values:

C#

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")]
int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});

7 Note

When binding a string[] from a query string, the absence of any matching query
string value will result in an empty array instead of a null value.

Parameter binding for argument lists with [AsParameters]


AsParametersAttribute enables simple parameter binding to types and not complex or
recursive model binding.
Consider the following code:

C#

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>


await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.

Consider the following GET endpoint:

C#

app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());

The following struct can be used to replace the preceding highlighted parameters:

C#

struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}

The refactored GET endpoint uses the preceding struct with the AsParameters
attribute:

C#
app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest
request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());

The following code shows additional endpoints in the app:

C#

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>


{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};

Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();

return Results.Created($"/todoitems/{todoItem.Id}", new


TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>


{
var todo = await Db.Todos.FindAsync(Id);

if (todo is null) return Results.NotFound();

todo.Name = Dto.Name;
todo.IsComplete = Dto.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(new TodoItemDTO(todo));
}

return Results.NotFound();
});
The following classes are used to refactor the parameter lists:

C#

class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}

The following code shows the refactored endpoints using AsParameters and the
preceding struct and classes:

C#

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest


request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};

request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();

return Results.Created($"/todoitems/{todoItem.Id}", new


TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest


request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);

if (todo is null) return Results.NotFound();

todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;

await request.Db.SaveChangesAsync();

return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest


request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}

return Results.NotFound();
});

The following record types can be used to replace the preceding parameters:

C#

record TodoItemRequest(int Id, TodoDb Db);


record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

Using a struct with AsParameters can be more performant than using a record type.

The complete sample code in the AspNetCore.Docs.Samples repository.

Custom Binding
There are two ways to customize parameter binding:

1. For route, query, and header binding sources, bind custom types by adding a static
TryParse method for the type.
2. Control the binding process by implementing a BindAsync method on a type.

TryParse
TryParse has two APIs:

C#

public static bool TryParse(string value, T out result);


public static bool TryParse(string value, IFormatProvider provider, T out
result);

The following code displays Point: 12.3, 10.1 with the URI /map?Point=12.3,10.1 :
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point


{
public double X { get; set; }
public double Y { get; set; }

public static bool TryParse(string? value, IFormatProvider? provider,


out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries |
StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}

point = null;
return false;
}
}

BindAsync

BindAsync has the following APIs:

C#

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo


parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

The following code displays SortBy:xyz, SortDirection:Desc, CurrentPage:99 with the


URI /products?SortBy=xyz&SortDir=Desc&Page=99 :
C#

using System.Reflection;

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy},
" +
$"SortDirection:{pageData.SortDirection}, CurrentPage:
{pageData.CurrentPage}");

app.Run();

public class PagingData


{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;

public static ValueTask<PagingData?> BindAsync(HttpContext context,


ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";

Enum.TryParse<SortDirection>
(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var
sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;

var result = new PagingData


{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};

return ValueTask.FromResult<PagingData?>(result);
}
}

public enum SortDirection


{
Default,
Asc,
Desc
}
Binding failures
When binding fails, the framework logs a debug message and returns various status
codes to the client depending on the failure mode.

Failure mode Nullable Parameter Binding Source Status


Type code

{ParameterType}.TryParse returns yes route/query/header 400


false

{ParameterType}.BindAsync returns yes custom 400


null

{ParameterType}.BindAsync throws does not matter custom 500

Failure to deserialize JSON body does not matter body 400

Wrong content type (not does not matter body 415


application/json )

Binding Precedence
The rules for determining a binding source from a parameter:

1. Explicit attribute defined on parameter (From* attributes) in the following order:


a. Route values: [FromRoute]
b. Query string: [FromQuery]
c. Header: [FromHeader]
d. Body: [FromBody]
e. Service: [FromServices]
f. Parameter values: [AsParameters]
2. Special types
a. HttpContext
b. HttpRequest (HttpContext.Request)
c. HttpResponse (HttpContext.Response)
d. ClaimsPrincipal (HttpContext.User)
e. CancellationToken (HttpContext.RequestAborted)
f. IFormFileCollection (HttpContext.Request.Form.Files)
g. IFormFile (HttpContext.Request.Form.Files[paramName])
h. Stream (HttpContext.Request.Body)
i. PipeReader (HttpContext.Request.BodyReader)
3. Parameter type has a valid static BindAsync method.
4. Parameter type is a string or has a valid static TryParse method.
a. If the parameter name exists in the route template e.g. app.Map("/todo/{id}",
(int id) => {}); , then it's bound from the route.
b. Bound from the query string.
5. If the parameter type is a service provided by dependency injection, it uses that
service as the source.
6. The parameter is from the body.

Configure JSON deserialization options for body binding


The body binding source uses System.Text.Json for deserialization. It is not possible to
change this default, but JSON serialization and deserialization options can be
configured.

Configure JSON deserialization options globally

Options that apply globally for an app can be configured by invoking


ConfigureHttpJsonOptions. The following example includes public fields and formats
JSON output.

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {


if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});

app.Run();

class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }

Since the sample code configures both serialization and deserialization, it can read
NameField and include NameField in the output JSON.

Configure JSON deserialization options for an endpoint

ReadFromJsonAsync has overloads that accept a JsonSerializerOptions object. The


following example includes public fields and formats JSON output.

C#

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {


IncludeFields = true,
WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {


if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});

app.Run();

class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }

Since the preceding code applies the customized options only to deserialization, the
output JSON excludes NameField .

Read the request body


Read the request body directly using a HttpContext or HttpRequest parameter:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest


request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"],
Path.GetRandomFileName());

await using var writeStream = File.Create(filePath);


await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

The preceding code:

Accesses the request body using HttpRequest.BodyReader.


Copies the request body to a local file.

Responses
Route handlers support the following types of return values:

1. IResult based - This includes Task<IResult> and ValueTask<IResult>


2. string - This includes Task<string> and ValueTask<string>
3. T (Any other type) - This includes Task<T> and ValueTask<T>

Return value Behavior Content-Type


Return value Behavior Content-Type

IResult The framework calls IResult.ExecuteAsync Decided by the IResult


implementation

string The framework writes the string directly to text/plain


the response

T (Any other The framework JSON-serializes the response application/json


type)

For a more in-depth guide to route handler return values see Create responses in
Minimal API applications

Example return values

string return values

C#

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

JSON return values

C#

app.MapGet("/hello", () => new { Message = "Hello World" });

Return TypedResults
The following code returns a TypedResults:

C#

app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello


World!" }));

Returning TypedResults is preferred to returning Results. For more information, see


TypedResults vs Results.

IResult return values


C#

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

The following example uses the built-in result types to customize the response:

C#

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>


await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);

JSON

C#

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

Custom Status Code

C#

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

Text

C#

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

C#

var proxyClient = new HttpClient();


app.MapGet("/pokemon", async () =>
{
var stream = await
proxyClient.GetStreamAsync("http://consoto/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});

See Create responses in Minimal API applications for more examples.

Redirect

C#

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

File

C#

app.MapGet("/download", () => Results.File("myfile.text"));

Built-in results
Common result helpers exist in the Results and TypedResults static classes. Returning
TypedResults is preferred to returning Results . For more information, see TypedResults
vs Results.

Customizing results
Applications can control responses by implementing a custom IResult type. The
following code is an example of an HTML result type:

C#

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions,
string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);

return new HtmlResult(html);


}
}
class HtmlResult : IResult
{
private readonly string _html;

public HtmlResult(string html)


{
_html = html;
}

public Task ExecuteAsync(HttpContext httpContext)


{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength =
Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}

We recommend adding an extension method to


Microsoft.AspNetCore.Http.IResultExtensions to make these custom results more
discoverable.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>


<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));

app.Run();

Typed results
The IResult interface can 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 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[]>));
}
}

You can look at the return types of the corresponding methods on the static
TypedResults class to find the correct public IResult type to cast to.

See Create responses in Minimal API applications for more examples.

Filters
See Filters in Minimal API apps

Authorization
Routes can be protected using authorization policies. These can be declared via the
[Authorize] attribute or by using the RequireAuthorization method:

C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));

var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires


authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this
endpoint.");

app.Run();

The preceding code can be written with RequireAuthorization:

C#

app.MapGet("/auth", () => "This endpoint requires authorization")


.RequireAuthorization();

The following sample uses policy-based authorization:

C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));

var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () =>


"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");


app.MapGet("/Identity/Account/Login", () => "Sign in page at this
endpoint.");

app.Run();

Allow unauthenticated users to access an endpoint


The [AllowAnonymous] allows unauthenticated users to access endpoints:

C#

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all


roles.");

app.MapGet("/login2", () => "This endpoint also for all roles.")


.AllowAnonymous();

CORS
Routes can be CORS enabled using CORS policies. CORS can be declared via the
[EnableCors] attribute or by using the RequireCors method. The following samples
enable CORS:

C#

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});

var app = builder.Build();


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

app.Run();

C#

using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});

var app = builder.Build();


app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>


"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);

app.Run();

For more information, see Enable Cross-Origin Requests (CORS) in ASP.NET Core

See also
How to use OpenAPI in Minimal API apps
Create responses in Minimal API applications
Filters in Minimal API apps
Handle errors in Minimal API apps
Authentication and authorization in minimal APIs
Test Minimal API apps
WebApplication and
WebApplicationBuilder in Minimal API
apps
Article • 04/19/2023

WebApplication
The following code is generated by an ASP.NET Core template:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

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

app.Run();

The preceding code can be created via dotnet new web on the command line or
selecting the Empty Web template in Visual Studio.

The following code creates a WebApplication ( app ) without explicitly creating a


WebApplicationBuilder:

C#

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create initializes a new instance of the WebApplication class with


preconfigured defaults.

WebApplication automatically adds the following middleware depending on certain


conditions:

UseDeveloperExceptionPage is added first when the HostingEnvironment is


"Development" .
UseRouting is added second if user code didn't already call UseRouting and if there
are endpoints configured, for example app.MapGet .
UseEndpoints is added at the end of the middleware pipeline if any endpoints are
configured.
UseAuthentication is added immediately after UseRouting if
IAuthenticationSchemeProvider can be detected in the service provider.
IAuthenticationSchemeProvider is added by default when using
AddAuthentication, and services are detected using IServiceProviderIsService.
UseAuthorization is added next if IAuthorizationHandlerProvider can be detected
in the service provider. IAuthorizationHandlerProvider is added by default when
using AddAuthorization, and services are detected using
IServiceProviderIsService .
User configured middleware and endpoints are added between UseRouting and
UseEndpoints .

The following code is effectively what the automatic middleware being added to the app
produces:

C#

if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

In some cases, the default middleware configuration isn't correct for the app and
requires modification. For example, UseCors should be called before UseAuthentication
and UseAuthorization. The app needs to call UseAuthentication and UseAuthorization if
UseCors is called:

C#

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

If middleware should be run before route matching occurs, UseRouting should be called
and the middleware should be placed before the call to UseRouting . UseEndpoints isn't
required in this case as it is automatically added as described previously:

C#

app.Use((context, next) =>


{
return next(context);
});

app.UseRouting();

// other middleware and endpoints

When adding a terminal middleware:

The middleware must be added after UseEndpoints .


The app needs to call UseRouting and UseEndpoints so that the terminal
middleware can be placed at the correct location.

C#

app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});

Terminal middleware is middleware that runs if no endpoint handles the request.


Working with ports
When a web app is created with Visual Studio or dotnet new , a
Properties/launchSettings.json file is created that specifies the ports the app responds

to. In the port setting samples that follow, running the app from Visual Studio returns an
error dialog Unable to connect to web server 'AppName' . Visual Studio returns an error
because it's expecting the port specified in Properties/launchSettings.json , but the
app is using the port specified by app.Run("http://localhost:3000") . Run the following
port changing samples from the command line.

The following sections set the port the app responds to.

C#

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

In the preceding code, the app responds to port 3000 .

Multiple ports

In the following code, the app responds to port 3000 and 4000 .

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

Set the port from the command line

The following command makes the app respond to port 7777 :

.NET CLI

dotnet run --urls="https://localhost:7777"


If the Kestrel endpoint is also configured in the appsettings.json file, the
appsettings.json file specified URL is used. For more information, see Kestrel endpoint
configuration

Read the port from environment


The following code reads the port from the environment:

C#

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

The preferred way to set the port from the environment is to use the ASPNETCORE_URLS
environment variable, which is shown in the following section.

Set the ports via the ASPNETCORE_URLS environment variable


The ASPNETCORE_URLS environment variable is available to set the port:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS supports multiple URLs:

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

Listen on all interfaces


The following samples demonstrate listening on all interfaces

http://*:3000
C#

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

C#

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

Listen on all interfaces using ASPNETCORE_URLS


The preceding samples can use ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Specify HTTPS with development certificate


C#

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

For more information on the development certificate, see Trust the ASP.NET Core HTTPS
development certificate on Windows and macOS.

Specify HTTPS using a custom certificate


The following sections show how to specify the custom certificate using the
appsettings.json file and via configuration.

Specify the custom certificate with appsettings.json

JSON

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}

Specify the custom certificate via configuration

C#

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key


builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Use the certificate APIs

C#

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath,
"cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath,
"key.pem");

httpsOptions.ServerCertificate =
X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Read the environment


C#

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}

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


app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

For more information using the environment, see Use multiple environments in ASP.NET
Core

Configuration
The following code reads from the configuration system:

C#

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Hello";

app.MapGet("/", () => message);

app.Run();

For more information, see Configuration in ASP.NET Core

Logging
The following code writes a message to the log on application startup:

C#

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

For more information, see Logging in .NET Core and ASP.NET Core

Access the Dependency Injection (DI) container


The following code shows how to get services from the DI container during application
startup:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())


{
var sampleService =
scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}

app.Run();

For more information, see Dependency injection in ASP.NET Core.

WebApplicationBuilder
This section contains sample code using WebApplicationBuilder.

Change the content root, application name, and


environment
The following code sets the content root, application name, and environment:

C#

var builder = WebApplication.CreateBuilder(new WebApplicationOptions


{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name:
{builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name:
{builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path:
{builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder


class with preconfigured defaults.

For more information, see ASP.NET Core fundamentals overview

Change the content root, app name, and environment by


environment variables or command line
The following table shows the environment variable and command-line argument used
to change the content root, app name, and environment:

feature Environment variable Command-line argument

Application name ASPNETCORE_APPLICATIONNAME --applicationName

Environment name ASPNETCORE_ENVIRONMENT --environment

Content root ASPNETCORE_CONTENTROOT --contentRoot

Add configuration providers


The following sample adds the INI configuration provider:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

For detailed information, see File configuration providers in Configuration in ASP.NET


Core.

Read configuration
By default the WebApplicationBuilder reads configuration from multiple sources,
including:

appSettings.json and appSettings.{environment}.json

Environment variables
The command line

For a complete list of configuration sources read, see Default configuration in


Configuration in ASP.NET Core

C#

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Read the environment


The following code reads HelloKey from configuration and displays the value at the /
endpoint. If the configuration value is null, "Hello" is assigned to message :

C#

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}

var app = builder.Build();

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

app.Run();

Add logging providers


C#
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.


builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

Add services
C#

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.


builder.Services.AddMemoryCache();

// Add a custom scoped service.


builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Customize the IHostBuilder


Existing extension methods on IHostBuilder can be accessed using the Host property:

C#

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.


builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout =
TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

Customize the IWebHostBuilder


Extension methods on IWebHostBuilder can be accessed using the
WebApplicationBuilder.WebHost property.

C#

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based


builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Change the web root


By default, the web root is relative to the content root in the wwwroot folder. Web root is
where the static files middleware looks for static files. Web root can be changed with
WebHostOptions , the command line, or with the UseWebRoot method:

C#

var builder = WebApplication.CreateBuilder(new WebApplicationOptions


{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

Custom dependency injection (DI) container


The following example uses Autofac :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't


// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Add Middleware
Any existing ASP.NET Core middleware can be configured on the WebApplication :

C#

var app = WebApplication.Create(args);

// Setup the file server to serve static files.


app.UseFileServer();

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

app.Run();

For more information, see ASP.NET Core Middleware

Developer exception page


WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder
class with preconfigured defaults. The developer exception page is enabled in the
preconfigured defaults. When the following code is run in the development
environment, navigating to / renders a friendly page that shows the exception.

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an
exception.");
});

app.Run();
Route Handlers in Minimal API apps
Article • 03/30/2023

A configured WebApplication supports Map{Verb} and MapMethods where {Verb} is a


camel-cased HTTP method like Get , Post , Put or Delete :

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", () => "This is a GET");


app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },


() => "This is an options or head request ");

app.Run();

The Delegate arguments passed to these methods are called "route handlers".

Route handlers
Route handlers are methods that execute when the route matches. Route handlers can
be a lambda expression, a local function, an instance method or a static method. Route
handlers can be synchronous or asynchronous.

Lambda expression
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();
Local function
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Instance method
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}

Static method
C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}

Endpoint defined outside of Program.cs


Minimal APIs don't have to be located in Program.cs .

Program.cs

C#

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

C#

namespace MinAPISeparateFile;

public static class TodoEndpoints


{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Get all todo items
await context.Response.WriteAsJsonAsync(new { Message = "All
todo items" });
});

app.MapGet("/{id}", async context =>


{
// Get one todo item
await context.Response.WriteAsJsonAsync(new { Message = "One
todo item" });
});
}
}
See also Route groups later in this article.

Named endpoints and link generation


Endpoints can be given names in order to generate URLs to the endpoint. Using a
named endpoint avoids having to hard code paths in an app:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")


.WithName("hi");

app.MapGet("/", (LinkGenerator linker) =>


$"The link to the hello route is {linker.GetPathByName("hi", values:
null)}");

app.Run();

The preceding code displays The link to the hello endpoint is /hello from the /
endpoint.

NOTE: Endpoint names are case sensitive.

Endpoint names:

Must be globally unique.


Are used as the OpenAPI operation id when OpenAPI support is enabled. For more
information, see OpenAPI.

Route Parameters
Route parameters can be captured as part of the route pattern definition:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is
{bookId}");

app.Run();
The preceding code returns The user id is 3 and book id is 7 from the URI
/users/3/books/7 .

The route handler can declare the parameters to capture. When a request is made a
route with parameters declared to capture, the parameters are parsed and passed to the
handler. This makes it easy to capture the values in a type safe way. In the preceding
code, userId and bookId are both int .

In the preceding code, if either route value cannot be converted to an int , an exception
is thrown. The GET request /users/hello/books/3 throws the following exception:

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Wildcard and catch all routes


The following catch all route returns Routing to hello from the `/posts/hello' endpoint:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Route constraints
Route constraints constrain the matching behavior of a route.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));


app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t =>
t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post
{slug}");

app.Run();

The following table demonstrates the preceding route templates and their behavior:
Route Template Example Matching URI

/todos/{id:int} /todos/1

/todos/{text} /todos/something

/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

For more information, see Route constraint reference in Routing in ASP.NET Core.

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

Parameter binding
Parameter binding in Minimal API applications describes the rules in detail for how route
handler parameters are populated.

Responses
Create responses in Minimal API applications describes in detail how values returned
from route handlers are converted into responses.
Parameter Binding in Minimal API apps
Article • 05/23/2023

Parameter binding is the process of converting request data into strongly typed
parameters that are expressed by route handlers. A binding source determines where
parameters are bound from. Binding sources can be explicit or inferred based on HTTP
method and parameter type.

Supported binding sources:

Route values
Query string
Header
Body (as JSON)
Services provided by dependency injection
Custom

Binding from form values is not natively supported in .NET 6 and 7.

The following GET route handler uses some of these parameter binding sources:

C#

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,


int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string
customHeader,
Service service) => { });

class Service { }

The following table shows the relationship between the parameters used in the
preceding example and the associated binding sources.

Parameter Binding Source

id route value

page query string


Parameter Binding Source

customHeader header

service Provided by dependency injection

The HTTP methods GET , HEAD , OPTIONS , and DELETE don't implicitly bind from body. To
bind from body (as JSON) for these HTTP methods, bind explicitly with [FromBody] or
read from the HttpRequest.

The following example POST route handler uses a binding source of body (as JSON) for
the person parameter:

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

The parameters in the preceding examples are all bound from request data
automatically. To demonstrate the convenience that parameter binding provides, the
following route handlers show how to read request data directly from the request:

C#

app.MapGet("/{id}", (HttpRequest request) =>


{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];

// ...
});

app.MapPost("/", async (HttpRequest request) =>


{
var person = await request.ReadFromJsonAsync<Person>();

// ...
});

Explicit Parameter Binding


Attributes can be used to explicitly declare where parameters are bound from.

C#

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", ([FromRoute] int id,


[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});

class Service { }

record Person(string Name, int Age);

Parameter Binding Source

id route value with the name id

page query string with the name "p"

service Provided by dependency injection

contentType header with the name "Content-Type"

7 Note

Binding from form values is not natively supported in .NET 6 and 7.

Parameter binding with dependency injection


Parameter binding for minimal APIs binds parameters through dependency injection
when the type is configured as a service. It's not necessary to explicitly apply the
[FromServices] attribute to a parameter. In the following code, both actions return the
time:

C#
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);


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

var app = builder.Build();

app.MapGet("/", ( IDateTime dateTime) => dateTime.Now);


app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

Optional parameters
Parameters declared in route handlers are treated as required:

If a request matches the route, the route handler only runs if all required
parameters are provided in the request.
Failure to provide all required parameters results in an error.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page


{pageNumber}");

app.Run();

URI result

/products? 3 returned
pageNumber=3

/products BadHttpRequestException : Required parameter "int pageNumber" was not


provided from query string.

/products/1 HTTP 404 error, no matching route

To make pageNumber optional, define the type as optional or provide a default value:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber


?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();

URI result

/products?pageNumber=3 3 returned

/products 1 returned

/products2 1 returned

The preceding nullable and default value applies to all sources:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();

The preceding code calls the method with a null product if no request body is sent.

NOTE: If invalid data is provided and the parameter is nullable, the route handler is not
run.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber


?? 1}");

app.Run();

URI result

/products? 3 returned
pageNumber=3
URI result

/products 1 returned

/products? BadHttpRequestException : Failed to bind parameter "Nullable<int>


pageNumber=two pageNumber" from "two".

/products/two HTTP 404 error, no matching route

See the Binding Failures section for more information.

Special types
The following types are bound without explicit attributes:

HttpContext: The context which holds all the information about the current HTTP
request or response:

C#

app.MapGet("/", (HttpContext context) =>


context.Response.WriteAsync("Hello World"));

HttpRequest and HttpResponse: The HTTP request and HTTP response:

C#

app.MapGet("/", (HttpRequest request, HttpResponse response) =>


response.WriteAsync($"Hello World {request.Query["name"]}"));

CancellationToken: The cancellation token associated with the current HTTP


request:

C#

app.MapGet("/", async (CancellationToken cancellationToken) =>


await MakeLongRunningRequestAsync(cancellationToken));

ClaimsPrincipal: The user associated with the request, bound from


HttpContext.User:

C#

app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);


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.

The following code implements a background queue:

C#

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService


{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;

public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,


ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken


stoppingToken)
{
await foreach (var dataStream in
_queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>
(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}

The following code binds the request body to a Stream :

C#

app.MapPost("/register", async (HttpRequest req, Stream body,


Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}

// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the
maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked
request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

var buffer = new byte[readSize];

// Read at least that many bytes from the body.


var read = await body.ReadAtLeastAsync(buffer, readSize,
throwOnEndOfStream: false);

// We read more than the max, so this is a bad request.


if (read > maxMessageSize)
{
return Results.BadRequest();
}

// Attempt to send the buffer to the background queue.


if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}

// We couldn't accept the message since we're overloaded.


return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

The following code shows the complete Program.cs file:


C#

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);


// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of
85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions


var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.


builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>
(maxQueueSize));

// Create a background queue service.


builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header


'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23,
"Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header
"Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\":
23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}

// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the
maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked
request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

var buffer = new byte[readSize];

// Read at least that many bytes from the body.


var read = await body.ReadAtLeastAsync(buffer, readSize,
throwOnEndOfStream: false);

// We read more than the max, so this is a bad request.


if (read > maxMessageSize)
{
return Results.BadRequest();
}

// Attempt to send the buffer to the background queue.


if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}

// We couldn't accept the message since we're overloaded.


return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();

When reading data, the Stream is the same object as HttpRequest.Body .


The request body isn't buffered by default. After the body is read, it's not
rewindable. The stream can't be read multiple times.
The Stream and PipeReader aren't usable outside of the minimal action handler as
the underlying buffers will be disposed or reused.

File uploads using 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.

Bind arrays and string values from headers and query


strings
The following code demonstrates binding query strings to an array of primitive types,
string arrays, and StringValues:

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. The following code binds to a string array and
returns all the items with the specified tags:

C#

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
The following code shows the model and the required TryParse implementation:

C#

public class Todo


{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }

// This is an owned entity.


public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";

public static bool TryParse(string? name, out Tag tag)


{
if (name is null)
{
tag = default!;
return false;
}

tag = new Tag { Name = name };


return true;
}
}

The following code binds to an int array:

C#

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});

To test the preceding code, add the following endpoint to populate the database with
Todo items:

C#
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();

return Results.Ok(todos);
});

Use a tool like Postman to pass the following data to the previous endpoint:

C#

[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]

The following code binds to the header key X-Todo-Id and returns the Todo items with
matching Id values:
C#

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")]
int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});

Parameter binding for argument lists with [AsParameters]


AsParametersAttribute enables simple parameter binding to types and not complex or
recursive model binding.

Consider the following code:

C#

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>


await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.

Consider the following GET endpoint:

C#

app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());

The following struct can be used to replace the preceding highlighted parameters:

C#

struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}

The refactored GET endpoint uses the preceding struct with the AsParameters
attribute:

C#

app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest
request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());

The following code shows additional endpoints in the app:

C#

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>


{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};

Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();

return Results.Created($"/todoitems/{todoItem.Id}", new


TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>


{
var todo = await Db.Todos.FindAsync(Id);

if (todo is null) return Results.NotFound();


todo.Name = Dto.Name;
todo.IsComplete = Dto.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(new TodoItemDTO(todo));
}

return Results.NotFound();
});

The following classes are used to refactor the parameter lists:

C#

class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}

The following code shows the refactored endpoints using AsParameters and the
preceding struct and classes:

C#

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest


request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();

return Results.Created($"/todoitems/{todoItem.Id}", new


TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest


request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);

if (todo is null) return Results.NotFound();

todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;

await request.Db.SaveChangesAsync();

return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest


request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}

return Results.NotFound();
});

The following record types can be used to replace the preceding parameters:

C#

record TodoItemRequest(int Id, TodoDb Db);


record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

Using a struct with AsParameters can be more performant than using a record type.

The complete sample code in the AspNetCore.Docs.Samples repository.

Custom Binding
There are two ways to customize parameter binding:

1. For route, query, and header binding sources, bind custom types by adding a static
TryParse method for the type.

2. Control the binding process by implementing a BindAsync method on a type.

TryParse

TryParse has two APIs:

C#

public static bool TryParse(string value, T out result);


public static bool TryParse(string value, IFormatProvider provider, T out
result);

The following code displays Point: 12.3, 10.1 with the URI /map?Point=12.3,10.1 :

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point


{
public double X { get; set; }
public double Y { get; set; }

public static bool TryParse(string? value, IFormatProvider? provider,


out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries |
StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}

point = null;
return false;
}
}

BindAsync
BindAsync has the following APIs:

C#

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo


parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

The following code displays SortBy:xyz, SortDirection:Desc, CurrentPage:99 with the


URI /products?SortBy=xyz&SortDir=Desc&Page=99 :

C#

using System.Reflection;

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy},
" +
$"SortDirection:{pageData.SortDirection}, CurrentPage:
{pageData.CurrentPage}");

app.Run();

public class PagingData


{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;

public static ValueTask<PagingData?> BindAsync(HttpContext context,


ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";

Enum.TryParse<SortDirection>
(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var
sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;

var result = new PagingData


{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};

return ValueTask.FromResult<PagingData?>(result);
}
}

public enum SortDirection


{
Default,
Asc,
Desc
}

Binding failures
When binding fails, the framework logs a debug message and returns various status
codes to the client depending on the failure mode.

Failure mode Nullable Parameter Binding Source Status


Type code

{ParameterType}.TryParse returns yes route/query/header 400


false

{ParameterType}.BindAsync returns yes custom 400


null

{ParameterType}.BindAsync throws does not matter custom 500

Failure to deserialize JSON body does not matter body 400

Wrong content type (not does not matter body 415


application/json )

Binding Precedence
The rules for determining a binding source from a parameter:

1. Explicit attribute defined on parameter (From* attributes) in the following order:


a. Route values: [FromRoute]
b. Query string: [FromQuery]
c. Header: [FromHeader]
d. Body: [FromBody]
e. Service: [FromServices]
f. Parameter values: [AsParameters]
2. Special types
a. HttpContext
b. HttpRequest (HttpContext.Request)
c. HttpResponse (HttpContext.Response)
d. ClaimsPrincipal (HttpContext.User)
e. CancellationToken (HttpContext.RequestAborted)
f. IFormFileCollection (HttpContext.Request.Form.Files)
g. IFormFile (HttpContext.Request.Form.Files[paramName])
h. Stream (HttpContext.Request.Body)
i. PipeReader (HttpContext.Request.BodyReader)
3. Parameter type has a valid static BindAsync method.
4. Parameter type is a string or has a valid static TryParse method.
a. If the parameter name exists in the route template e.g. app.Map("/todo/{id}",
(int id) => {}); , then it's bound from the route.

b. Bound from the query string.


5. If the parameter type is a service provided by dependency injection, it uses that
service as the source.
6. The parameter is from the body.

Configure JSON deserialization options for body binding


The body binding source uses System.Text.Json for deserialization. It is not possible to
change this default, but JSON serialization and deserialization options can be
configured.

Configure JSON deserialization options globally


Options that apply globally for an app can be configured by invoking
ConfigureHttpJsonOptions. The following example includes public fields and formats
JSON output.

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {


if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});

app.Run();

class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }

Since the sample code configures both serialization and deserialization, it can read
NameField and include NameField in the output JSON.

Configure JSON deserialization options for an endpoint


ReadFromJsonAsync has overloads that accept a JsonSerializerOptions object. The
following example includes public fields and formats JSON output.

C#

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {


IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});

app.Run();

class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }

Since the preceding code applies the customized options only to deserialization, the
output JSON excludes NameField .

Read the request body


Read the request body directly using a HttpContext or HttpRequest parameter:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest


request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"],
Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

The preceding code:

Accesses the request body using HttpRequest.BodyReader.


Copies the request body to a local file.
How to create responses in Minimal API
apps
Article • 04/14/2023

Minimal endpoints support the following types of return values:

1. string - This includes Task<string> and ValueTask<string> .


2. T (Any other type) - This includes Task<T> and ValueTask<T> .
3. IResult based - This includes Task<IResult> and ValueTask<IResult> .

string return values


Behavior Content-Type

The framework writes the string directly to the response. text/plain

Consider the following route handler, which returns a Hello world text.

C#

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

The 200 status code is returned with text/plain Content-Type header and the following
content.

text

Hello World

T (Any other type) return values


Behavior Content-Type

The framework JSON-serializes the response. application/json

Consider the following route handler, which returns an anonymous type containing a
Message string property.

C#
app.MapGet("/hello", () => new { Message = "Hello World" });

The 200 status code is returned with application/json Content-Type header and the
following content.

JSON

{"message":"Hello World"}

IResult return values


Behavior Content-Type

The framework calls IResult.ExecuteAsync. Decided by the IResult implementation.

The IResult interface defines a contract that represents the result of an HTTP endpoint.
The static Results class and the static TypedResults are used to create various IResult
objects that represent different types of responses.

TypedResults vs Results
The Results and TypedResults static classes provide similar sets of results helpers. The
TypedResults class is the typed equivalent of the Results class. However, the Results

helpers' return type is IResult, while each TypedResults helper's return type is one of the
IResult implementation types. The difference means that for Results helpers a

conversion is needed when the concrete type is needed, for example, for unit testing.
The implementation types are defined in the Microsoft.AspNetCore.Http.HttpResults
namespace.

Returning TypedResults rather than Results has the following advantages:

TypedResults helpers return strongly typed objects, which can improve code

readability, unit testing, and reduce the chance of runtime errors.


The implementation type automatically provides the response type metadata for
OpenAPI to describe the endpoint.

Consider the following endpoint, for which a 200 OK status code with the expected
JSON response is produced.

C#
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!"
}))
.Produces<Message>();

In order to document this endpoint correctly the extensions method Produces is called.
However, it's not necessary to call Produces if TypedResults is used instead of Results ,
as shown in the following code. TypedResults automatically provides the metadata for
the endpoint.

C#

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello


World!" }));

For more information about describing a response type, see OpenAPI support in
minimal APIs.

As mentioned previously, when using TypedResults , a conversion is not needed.


Consider the following minimal API which returns a TypedResults class

C#

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext


database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}

The following test checks for the full concrete type:

C#

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

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

await context.SaveChangesAsync();

// Act
var result = await TodoEndpointsV1.GetAllTodos(context);

//Assert
Assert.IsType<Ok<Todo[]>>(result);

Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}

Because all methods on Results return IResult in their signature, the compiler
automatically infers that as the request delegate return type when returning different
results from a single endpoint. TypedResults requires the use of Results<T1, TN> from
such delegates.

The following method compiles because both Results.Ok and Results.NotFound are
declared as returning IResult , even though the actual concrete types of the objects
returned are different:

C#

app.MapGet("/todos/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id,


TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
The following method does not compile, because TypedResults.Ok and
TypedResults.NotFound are declared as returning different types and the compiler won't
attempt to infer the best matching type:

C#

app.MapGet("/todos/{id}", async (int id, TodoDb db) =>


await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());

To use TypedResults , the return type must be fully declared, which when asynchronous
requires the Task<> wrapper. Using TypedResults is more verbose, but that's the trade-
off for having the type information be statically available and thus capable of self-
describing to OpenAPI:

C#

app.MapGet("/todos/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id,


TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());

Results<TResult1, TResultN>
Use Results<TResult1, TResultN> as the endpoint handler return type instead of
IResult when:

Multiple IResult implementation types are returned from the endpoint handler.
The static TypedResult class is used to create the IResult objects.

This alternative is better than returning IResult because the generic union types
automatically retain the endpoint metadata. And since the Results<TResult1, TResultN>
union types implement implicit cast operators, the compiler can automatically convert
the types specified in the generic arguments to an instance of the union type.

This has the added benefit of providing compile-time checking that a route handler
actually only returns the results that it declares it does. Attempting to return a type that
isn't declared as one of the generic arguments to Results<> results in a compilation
error.
Consider the following endpoint, for which a 400 BadRequest status code is returned
when the orderId is greater than 999 . Otherwise, it produces a 200 OK with the
expected content.

C#

app.MapGet("/orders/{orderId}", IResult (int orderId)


=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new
Order(orderId)))
.Produces(400)
.Produces<Order>();

In order to document this endpoint correctly the extension method Produces is called.
However, since the TypedResults helper automatically includes the metadata for the
endpoint, you can return the Results<T1, Tn> union type instead, as shown in the
following code.

C#

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)


=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new
Order(orderId)));

Built-in results
Common result helpers exist in the Results and TypedResults static classes. Returning
TypedResults is preferred to returning Results . For more information, see TypedResults
vs Results.

The following sections demonstrate the usage of the common result helpers.

JSON

C#

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync is an alternative way to return JSON:

C#

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync


(new { Message = "Hello World" }));
Custom Status Code

C#

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

Text

C#

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

var proxyClient = new HttpClient();


app.MapGet("/pokemon", async () =>
{
var stream = await
proxyClient.GetStreamAsync("http://consoto/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});

app.Run();

Results.Stream overloads allow access to the underlying HTTP response stream without
buffering. 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);
}

The following example streams an image from Azure Blob storage:

C#

app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token)
=>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new
BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken:
token), "image/jpeg");
});

The following example streams a video from an Azure Blob:

C#

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName,
string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new
BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);

var properties = await blobClient.GetPropertiesAsync(cancellationToken:


token);

DateTimeOffset lastModified = properties.Value.LastModified;


long length = properties.Value.ContentLength;

long etagHash = lastModified.ToFileTime() ^ length;


var entityTag = new EntityTagHeaderValue('\"' +
Convert.ToString(etagHash, 16) + '\"');

http.Response.Headers.CacheControl = $"public,max-age=
{TimeSpan.FromHours(24).TotalSeconds}";

return Results.Stream(await blobClient.OpenReadAsync(cancellationToken:


token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});

Redirect

C#

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

File

C#

app.MapGet("/download", () => Results.File("myfile.text"));

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>
Here's an example of a filter that uses one of these interfaces:

C#

app.MapGet("/weatherforecast", (int days) =>


{
if (days <= 0)
{
return Results.BadRequest();
}

var forecast = Enumerable.Range(1, days).Select(index =>


new WeatherForecast(DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);

return result switch


{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new
WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});

For more information, see Filters in Minimal API apps and IResult implementation types.

Customizing responses
Applications can control responses by implementing a custom IResult type. The
following code is an example of an HTML result type:

C#

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions,
string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);

return new HtmlResult(html);


}
}
class HtmlResult : IResult
{
private readonly string _html;

public HtmlResult(string html)


{
_html = html;
}

public Task ExecuteAsync(HttpContext httpContext)


{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength =
Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}

We recommend adding an extension method to


Microsoft.AspNetCore.Http.IResultExtensions to make these custom results more
discoverable.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>


<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));

app.Run();

Also, a custom IResult type can provide its own annotation by implementing the
IEndpointMetadataProvider interface. For example, the following code adds an
annotation to the preceding HtmlResult type that describe the response produced by
the endpoint.

C#

class HtmlResult : IResult, IEndpointMetadataProvider


{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}

public Task ExecuteAsync(HttpContext httpContext)


{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength =
Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}

public static void PopulateMetadata(MethodInfo method, EndpointBuilder


builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}

The ProducesHtmlMetadata is an implementation of IProducesResponseTypeMetadata


that defines the produced response content type text/html and the status code 200 OK .

C#

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata


{
public Type? Type => null;

public int StatusCode => 200;

public IEnumerable<string> ContentTypes { get; } = new[] {


MediaTypeNames.Text.Html };
}

An alternative approach is using the Microsoft.AspNetCore.Mvc.ProducesAttribute to


describe the produced response. The following code changes the PopulateMetadata
method to use ProducesAttribute .

C#

public static void PopulateMetadata(MethodInfo method, EndpointBuilder


builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Configure JSON serialization options


By default, minimal API apps use Web defaults options during JSON serialization and
deserialization.

Configure JSON serialization options globally


Options can be configured globally for an app by invoking ConfigureHttpJsonOptions.
The following example includes public fields and formats JSON output.

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {


if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});

app.Run();

class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }

Since fields are included, the preceding code reads NameField and includes it in the
output JSON.

Configure JSON serialization options for an endpoint


To configure serialization options for an endpoint, invoke Results.Json and pass to it a
JsonSerializerOptions object, as shown in the following example:

C#

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)


{ WriteIndented = true };

app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false },
options));

app.Run();

class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }

As an alternative, use an overload of WriteAsJsonAsync that accepts a


JsonSerializerOptions object. The following example uses this overload to format the
output JSON:

C#

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {


WriteIndented = true };

app.MapGet("/", (HttpContext context) =>


context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }

Additional Resources
Authentication and authorization in minimal APIs
OpenAPI support in minimal API apps
Article • 12/01/2022

The OpenAPI specification is a programming language-agnostic standard for


documenting HTTP APIs. This standard is supported in minimal APIs through a
combination of built-in APIs and open-source libraries. There are three key aspects to
OpenAPI integration in an application:

Generating information about the endpoints in the app.


Gathering the information into a format that matches the OpenAPI schema.
Exposing the generated OpenAPI schema via a visual UI or a serialized file.

Minimal APIs provide built-in support for generating information about endpoints in an
app via the Microsoft.AspNetCore.OpenApi package. Exposing the generated OpenAPI
definition via a visual UI requires a third-party package.

The following code is generated by the ASP.NET Core minimal web API template and
uses OpenAPI:

C#

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]


{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot",
"Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string?


Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

In the preceding highlighted code:

Microsoft.AspNetCore.OpenApi is explained in the next section.

AddEndpointsApiExplorer : Configures the app to use the API Explorer to discover


and describe endpoints with default annotations. WithOpenApi overrides matching,
default annotations generated by the API Explorer with those produced from the
Microsoft.AspNetCore.OpenApi package.
UseSwagger adds the Swagger middleware.

UseSwaggerUI enables the Static File Middleware.


WithName: The IEndpointNameMetadata on the endpoint is used for link
generation and is treated as the operation ID in the given endpoint's OpenAPI
specification.
WithOpenApi is explained later in this article.

Microsoft.AspNetCore.OpenApi NuGet package


ASP.NET Core provides the Microsoft.AspNetCore.OpenApi package to interact 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.

Microsoft.AspNetCore.OpenApi is added as a PackageReference to a project file:

XML
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.*-
*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

</Project>

When using Swashbuckle.AspNetCore with Microsoft.AspNetCore.OpenApi ,


Swashbuckle.AspNetCore 6.4.0 or later must be used. Microsoft.OpenApi 1.4.3 or later
must be used to leverage copy constructors in WithOpenApi invocations.

Add OpenAPI annotations to endpoints via


WithOpenApi
Calling WithOpenApi on the endpoint adds to the endpoint's metadata. This metadata
can be:

Consumed in third-party packages like Swashbuckle.AspNetCore .


Displayed in the Swagger user interface or in YAML or JSON generated to define
the API.

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();

Modify the OpenAPI annotation in WithOpenApi


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;
});

Add operation IDs to OpenAPI


Operation IDs are used to uniquely identify a given endpoint in OpenAPI. The
WithName extension method can be used to set the operation ID used for a method.

C#

app.MapGet("/todoitems2", async (TodoDb db) =>


await db.Todos.ToListAsync())
.WithName("GetToDoItems");

Alternatively, the OperationId property can be set directly on the OpenAPI annotation.

C#

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())


.WithOpenApi(operation => new(operation)
{
OperationId = "GetTodos"
});

Add tags to the OpenAPI description


OpenAPI supports using tag objects to categorize operations. These tags are typically
used to group operations in the Swagger UI. These tags can be added to an operation
by invoking the WithTags extension method on the endpoint with the desired tags.

C#

app.MapGet("/todoitems", async (TodoDb db) =>


await db.Todos.ToListAsync())
.WithTags("TodoGroup");

Alternatively, the list of OpenApiTags can be set on the OpenAPI annotation via the
WithOpenApi extension method.

C#

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())


.WithOpenApi(operation => new(operation)
{
Tags = new List<OpenApiTag> { new() { Name = "Todos" } }
});

Add endpoint summary or description


The endpoint summary and description can be added by invoking the WithOpenApi
extension method. In the following code, the summaries are set directly on the OpenAPI
annotation.

C#

app.MapGet("/todoitems2", async (TodoDb db) => await db.Todos.ToListAsync())


.WithOpenApi(operation => new(operation)
{
Summary = "This is a summary",
Description = "This is a description"
});

Exclude OpenAPI description


In the following sample, the /skipme endpoint is excluded from generating an OpenAPI
description:

C#
using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/swag", () => "Hello Swagger!")


.WithOpenApi();
app.MapGet("/skipme", () => "Skipping Swagger.")
.ExcludeFromDescription();

app.Run();

Mark an API as obsolete


To mark an endpoint as obsolete, set the Deprecated property on the OpenAPI
annotation.

C#

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())


.WithOpenApi(operation => new(operation)
{
Deprecated = true
});

Describe response types


OpenAPI supports providing a description of the responses returned from an API.
Minimal APIs support three strategies for setting the response type of an endpoint:

Via the Produces extension method on the endpoint


Via the ProducesResponseType attribute on the route handler
By returning TypedResults from the route handler
The Produces extension method can be used to add Produces metadata to an endpoint.
When no parameters are provided, the extension method populates metadata for the
targeted type under a 200 status code and an application/json content type.

C#

app
.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
.Produces<IList<Todo>>();

Using TypedResults in the implementation of an endpoint's route handler automatically


includes the response type metadata for the endpoint. For example, the following code
automatically annotates the endpoint with a response under the 200 status code with
an application/json content type.

C#

app.MapGet("/todos", async (TodoDb db) =>


{
var todos = await db.Todos.ToListAsync());
return TypedResults.Ok(todos);
});

Set responses for ProblemDetails


When setting the response type for endpoints that may return a ProblemDetails
response, the ProducesProblem extension method or TypedResults.Problem can be used
to add the appropriate annotation to the endpoint's metadata.

When there are no explicit annotations provided by one of the strategies above, the
framework attempts to determine a default response type by examining the signature of
the response. This default response is populated under the 200 status code in the
OpenAPI definition.

Multiple response types


If an endpoint can return different response types in different scenarios, you can provide
metadata in the following ways:

Call the Produces extension method multiple times, as shown in the following
example:

C#
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);

Use Results<TResult1,TResult2,TResultN> in the signature and TypedResults in the


body of the handler, as shown in the following example:

C#

app.MapGet("/book{id}", Results<Ok<Book>, NotFound> (int id, List<Book>


bookList) =>
{
return bookList.FirstOrDefault((i) => i.Id == id) is Book book
? TypedResults.Ok(book)
: TypedResults.NotFound();
});

The Results<TResult1,TResult2,TResultN> union types declare that a route


handler returns multiple IResult -implementing concrete types, and any of those
types that implement IEndpointMetadataProvider will contribute to the endpoint’s
metadata.

The union types implement implicit cast operators. These operators enable the
compiler to automatically convert the types specified in the generic arguments to
an instance of the union type. This capability has the added benefit of providing
compile-time checking that a route handler only returns the results that it declares
it does. Attempting to return a type that isn't declared as one of the generic
arguments to Results<TResult1,TResult2,TResultN> results in a compilation error.

Describe request body and parameters


In addition to describing the types that are returned by an endpoint, OpenAPI also
supports annotating the inputs that are consumed by an API. These inputs fall into two
categories:

Parameters that appear in the path, query string, headers, or cookies


Data transmitted as part of the request body

The framework infers the types for request parameters in the path, query, and header
string automatically based on the signature of the route handler.
To define the type of inputs transmitted as the request body, configure the properties
by using the Accepts extension method to define the object type and content type that
are expected by the request handler. In the following example, the endpoint accepts a
Todo object in the request body with an expected content-type of application/xml .

C#

app.MapPost("/todos/{id}", (int id, Todo todo) => ...)


.Accepts<Todo>("application/xml");

In addition to the Accepts extension method, A parameter type can describe its own
annotation by implementing the IEndpointParameterMetadataProvider interface. For
example, the following Todo type adds an annotation that requires a request body with
an application/xml content-type.

C#

public class Todo : IEndpointParameterMetadataProvider


{
public static void PopulateMetadata(ParameterInfo parameter,
EndpointBuilder builder)
{
builder.Metadata.Add(new ConsumesAttribute(typeof(Todo), isOptional:
false, "application/xml"));
}
}

When no explicit annotation is provided, the framework attempts to determine the


default request type if there's a request body parameter in the endpoint handler. The
inference uses the following heuristics to produce the annotation:

Request body parameters that are read from a form via the [FromForm] attribute
are described with the multipart/form-data content-type.
All other request body parameters are described with the application/json
content-type.
The request body is treated as optional if it's nullable or if the AllowEmpty
property is set on the FromBody attribute.

Support API versioning


Minimal APIs support API versioning via the Asp.Versioning.Http package . Examples of
configuring versioning with minimal APIs can be found in the API versioning repo .
ASP.NET Core OpenAPI source code on GitHub
WithOpenApi
OpenApiGenerator

Additional Resources
Authentication and authorization in minimal APIs
Filters in Minimal API apps
Article • 03/17/2023

By Fiyaz Bin Hasan , Martin Costello , and Rick Anderson

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

Running code before and after the endpoint handler.


Inspecting and modifying parameters provided during an endpoint handler
invocation.
Intercepting the response behavior of an endpoint 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.

Filters can be registered by providing a Delegate that takes a


EndpointFilterInvocationContext and returns a EndpointFilterDelegate . The
EndpointFilterInvocationContext provides access to the HttpContext of the request
and an Arguments list indicating the arguments passed to the handler in the order in
which they appear in the declaration of the handler.

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

string ColorName(string color) => $"Color specified: {color}!";

app.MapGet("/colorSelector/{color}", ColorName)
.AddEndpointFilter(async (invocationContext, next) =>
{
var color = invocationContext.GetArgument<string>(0);

if (color == "Red")
{
return Results.Problem("Red not allowed!");
}
return await next(invocationContext);
});

app.Run();
The preceding code:

Calls the AddEndpointFilter extension method to add a filter to the


/colorSelector/{color} endpoint.

Returns the color specified except for the value "Red" .


Returns Results.Problem when the /colorSelector/Red is requested.
Uses next as the EndpointFilterDelegate and invocationContext as the
EndpointFilterInvocationContext to invoke the next filter in the pipeline or the
request delegate if the last filter has been invoked.

The filter is run before the endpoint handler. When multiple AddEndpointFilter
invocations are made on a handler:

Filter code called before the EndpointFilterDelegate ( next ) is called are executed
in order of First In, First Out (FIFO) order.
Filter code called after the EndpointFilterDelegate ( next ) is called are executed in
order of First In, Last Out (FILO) order.

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
app.Logger.LogInformation(" Endpoint");
return "Test of multiple filters";
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation("Before first filter");
var result = await next(efiContext);
app.Logger.LogInformation("After first filter");
return result;
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation(" Before 2nd filter");
var result = await next(efiContext);
app.Logger.LogInformation(" After 2nd filter");
return result;
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation(" Before 3rd filter");
var result = await next(efiContext);
app.Logger.LogInformation(" After 3rd filter");
return result;
});

app.Run();

In the preceding code, the filters and endpoint log the following output:

.NET CLI

Before first filter


Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filter

The following code uses filters that implement the IEndpointFilter interface:

C#

using Filters.EndpointFilters;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
app.Logger.LogInformation("Endpoint");
return "Test of multiple filters";
})
.AddEndpointFilter<AEndpointFilter>()
.AddEndpointFilter<BEndpointFilter>()
.AddEndpointFilter<CEndpointFilter>();

app.Run();

In the preceding code, the filters and handlers logs show the order they are run:

.NET CLI

AEndpointFilter Before next


BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
Filters implementing the IEndpointFilter interface are shown in the following example:

C#

namespace Filters.EndpointFilters;

public abstract class ABCEndpointFilters : IEndpointFilter


{
protected readonly ILogger Logger;
private readonly string _methodName;

protected ABCEndpointFilters(ILoggerFactory loggerFactory)


{
Logger = loggerFactory.CreateLogger<ABCEndpointFilters>();
_methodName = GetType().Name;
}

public virtual async ValueTask<object?>


InvokeAsync(EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Logger.LogInformation("{MethodName} Before next", _methodName);
var result = await next(context);
Logger.LogInformation("{MethodName} After next", _methodName);
return result;
}
}

class AEndpointFilter : ABCEndpointFilters


{
public AEndpointFilter(ILoggerFactory loggerFactory) :
base(loggerFactory) { }
}

class BEndpointFilter : ABCEndpointFilters


{
public BEndpointFilter(ILoggerFactory loggerFactory) :
base(loggerFactory) { }
}

class CEndpointFilter : ABCEndpointFilters


{
public CEndpointFilter(ILoggerFactory loggerFactory) :
base(loggerFactory) { }
}

Validate an object with a filter


Consider a filter that validates a Todo object:
C#

app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, 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();
}).AddEndpointFilter(async (efiContext, next) =>
{
var tdparam = efiContext.GetArgument<Todo>(0);

var validationError = Utilities.IsValid(tdparam);

if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
});

In the preceding code:

The EndpointFilterInvocationContext object provides access to the parameters


associated with a particular request issued to the endpoint via the GetArguments
method.
The filter is registered using a delegate that takes a
EndpointFilterInvocationContext and returns a EndpointFilterDelegate .

In addition to being passed as delegates, filters can be registered by implementing the


IEndpointFilter interface. The follow code shows the preceding filter encapsulated in a

class which implements IEndpointFilter :

C#

public class TodoIsValidFilter : IEndpointFilter


{
private ILogger _logger;

public TodoIsValidFilter(ILoggerFactory loggerFactory)


{
_logger = loggerFactory.CreateLogger<TodoIsValidFilter>();
}
public async ValueTask<object?>
InvokeAsync(EndpointFilterInvocationContext efiContext,
EndpointFilterDelegate next)
{
var todo = efiContext.GetArgument<Todo>(0);

var validationError = Utilities.IsValid(todo!);

if (!string.IsNullOrEmpty(validationError))
{
_logger.LogWarning(validationError);
return Results.Problem(validationError);
}
return await next(efiContext);
}
}

Filters that implement the IEndpointFilter interface can resolve dependencies from
Dependency Injection(DI), as shown in the previous code. Although filters can resolve
dependencies from DI, filters themselves can not be resolved from DI.

The ToDoIsValidFilter is applied to the following endpoints:

C#

app.MapPut("/todoitems2/{id}", async (Todo inputTodo, int id, 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();
}).AddEndpointFilter<TodoIsValidFilter>();

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


{
db.Todos.Add(todo);
await db.SaveChangesAsync();

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


}).AddEndpointFilter<TodoIsValidFilter>();

The following filter validates the Todo object and modifies the Name property:

C#
public class TodoIsValidUcFilter : IEndpointFilter
{
public async ValueTask<object?>
InvokeAsync(EndpointFilterInvocationContext efiContext,
EndpointFilterDelegate next)
{
var todo = efiContext.GetArgument<Todo>(0);
todo.Name = todo.Name!.ToUpper();

var validationError = Utilities.IsValid(todo!);

if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
}
}

Register a filter using an endpoint filter factory


In some scenarios, it might be necessary to cache some of the information provided in
the MethodInfo in a filter. For example, let's assume that we wanted to verify that the
handler an endpoint filter is attached to has a first parameter that evaluates to a Todo
type.

C#

app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, 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();
}).AddEndpointFilterFactory((filterFactoryContext, next) =>
{
var parameters = filterFactoryContext.MethodInfo.GetParameters();
if (parameters.Length >= 1 && parameters[0].ParameterType ==
typeof(Todo))
{
return async invocationContext =>
{
var todoParam = invocationContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(todoParam);

if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(invocationContext);
};
}
return invocationContext => next(invocationContext);
});

In the preceding code:

The EndpointFilterFactoryContext object provides access to the MethodInfo


associated with the endpoint's handler.
The signature of the handler is examined by inspecting MethodInfo for the
expected type signature. If the expected signature is found, the validation filter is
registered onto the endpoint. This factory pattern is useful to register a filter that
depends on the signature of the target endpoint handler.
If a matching signature isn't found, then a pass-through filter is registered.

Register a filter on controller actions


In some scenarios, it might be necessary to apply the same filter logic for both route-
handler based endpoints and controller actions. For this scenario, it is possible to invoke
AddEndpointFilter on ControllerActionEndpointConventionBuilder to support executing

the same filter logic on actions and endpoints.

C#

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapController()
.AddEndpointFilter(async (efiContext, next) =>
{
efiContext.HttpContext.Items["endpointFilterCalled"] = true;
var result = await next(efiContext);
return result;
});

app.Run();
Additional Resources
View or download sample code (how to download)
ValidationFilterRouteHandlerBuilderExtensions Validation extension methods.
Tutorial: Create a minimal API with ASP.NET Core
Authentication and authorization in minimal APIs
Unit and integration tests in Minimal
API apps
Article • 11/04/2022

By Fiyaz Bin Hasan , and Rick Anderson

Introduction to integration tests


Integration tests evaluate an app's components on a broader level than unit tests. Unit
tests are used to test isolated software components, such as individual class methods.
Integration tests confirm that two or more app components work together to produce
an expected result, possibly including every component required to fully process a
request.

These broader tests are used to test the app's infrastructure and whole framework, often
including the following components:

Database
File system
Network appliances
Request-response pipeline

Unit tests use fabricated components, known as fakes or mock objects, in place of
infrastructure components.

In contrast to unit tests, integration tests:

Use the actual components that the app uses in production.


Require more code and data processing.
Take longer to run.

Therefore, limit the use of integration tests to the most important infrastructure
scenarios. If a behavior can be tested using either a unit test or an integration test,
choose the unit test.

In discussions of integration tests, the tested project is frequently called the System
Under Test, or "SUT" for short. "SUT" is used throughout this article to refer to the
ASP.NET Core app being tested.

Don't write integration tests for every permutation of data and file access with
databases and file systems. Regardless of how many places across an app interact with
databases and file systems, a focused set of read, write, update, and delete integration
tests are usually capable of adequately testing database and file system components.
Use unit tests for routine tests of method logic that interact with these components. In
unit tests, the use of infrastructure fakes or mocks result in faster test execution.

ASP.NET Core integration tests


Integration tests in ASP.NET Core require the following:

A test project is used to contain and execute the tests. The test project has a
reference to the SUT.
The test project creates a test web host for the SUT and uses a test server client to
handle requests and responses with the SUT.
A test runner is used to execute the tests and report the test results.

Integration tests follow a sequence of events that include the usual Arrange, Act, and
Assert test steps:

1. The SUT's web host is configured.


2. A test server client is created to submit requests to the app.
3. The Arrange test step is executed: The test app prepares a request.
4. The Act test step is executed: The client submits the request and receives the
response.
5. The Assert test step is executed: The actual response is validated as a pass or fail
based on an expected response.
6. The process continues until all of the tests are executed.
7. The test results are reported.

Usually, the test web host is configured differently than the app's normal web host for
the test runs. For example, a different database or different app settings might be used
for the tests.

Infrastructure components, such as the test web host and in-memory test server
(TestServer), are provided or managed by the Microsoft.AspNetCore.Mvc.Testing
package. Use of this package streamlines test creation and execution.

The Microsoft.AspNetCore.Mvc.Testing package handles the following tasks:

Copies the dependencies file ( .deps ) from the SUT into the test project's bin
directory.
Sets the content root to the SUT's project root so that static files and pages/views
are found when the tests are executed.
Provides the WebApplicationFactory class to streamline bootstrapping the SUT
with TestServer .

The unit tests documentation describes how to set up a test project and test runner,
along with detailed instructions on how to run tests and recommendations for how to
name tests and test classes.

Separate unit tests from integration tests into different projects. Separating the tests:

Helps ensure that infrastructure testing components aren't accidentally included in


the unit tests.
Allows control over which set of tests are run.

The sample code on GitHub provides an example of unit and integration tests on a
Minimal API app.

IResult implementation types


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

The following code uses the NotFound<TValue> class:

C#

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

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

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

var notFoundResult = (NotFound) result.Result;

Assert.NotNull(notFoundResult);
}

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);
}

Additional Resources
Basic authentication tests is not a .NET repository but was written by a member
of the .NET team. It provides examples of basic authentication testing.
View or download sample code
Authentication and authorization in minimal APIs
Use port tunneling Visual Studio to debug web APIs
Test controller logic in ASP.NET Core
Razor Pages unit tests in ASP.NET Core
Middleware in Minimal API apps
Article • 04/19/2023

WebApplication automatically adds the following middleware depending on certain


conditions:

UseDeveloperExceptionPage is added first when the HostingEnvironment is


"Development" .

UseRouting is added second if user code didn't already call UseRouting and if there
are endpoints configured, for example app.MapGet .
UseEndpoints is added at the end of the middleware pipeline if any endpoints are
configured.
UseAuthentication is added immediately after UseRouting if
IAuthenticationSchemeProvider can be detected in the service provider.
IAuthenticationSchemeProvider is added by default when using
AddAuthentication, and services are detected using IServiceProviderIsService.
UseAuthorization is added next if IAuthorizationHandlerProvider can be detected
in the service provider. IAuthorizationHandlerProvider is added by default when
using AddAuthorization, and services are detected using
IServiceProviderIsService .
User configured middleware and endpoints are added between UseRouting and
UseEndpoints .

The following code is effectively what the automatic middleware being added to the app
produces:

C#

if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
app.UseAuthorization();
}
// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

In some cases, the default middleware configuration isn't correct for the app and
requires modification. For example, UseCors should be called before UseAuthentication
and UseAuthorization. The app needs to call UseAuthentication and UseAuthorization if
UseCors is called:

C#

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

If middleware should be run before route matching occurs, UseRouting should be called
and the middleware should be placed before the call to UseRouting . UseEndpoints isn't
required in this case as it is automatically added as described previously:

C#

app.Use((context, next) =>


{
return next(context);
});

app.UseRouting();

// other middleware and endpoints

When adding a terminal middleware:

The middleware must be added after UseEndpoints .


The app needs to call UseRouting and UseEndpoints so that the terminal
middleware can be placed at the correct location.

C#

app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});


app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});

Terminal middleware is middleware that runs if no endpoint handles the request.

For more information about middleware see ASP.NET Core Middleware, and the list of
built-in middleware that can be added to applications.
How to handle errors in Minimal API
apps
Article • 05/02/2023

With contributions by David Acker

This article describes how to handle errors in Minimal API apps.

Exceptions
In a Minimal API app, there are two different built-in centralized mechanisms to handle
unhandled exceptions:

Developer Exception Page middleware (For use in the Development environment


only.)
Exception handler middleware

This section refers to the following Minimal API app to demonstrate ways to handle
exceptions. It throws an exception when the endpoint /exception is requested:

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.Map("/exception", ()
=> { throw new InvalidOperationException("Sample Exception"); });

app.Run();

Developer Exception Page


The Developer Exception Page shows detailed stack traces for server errors. It uses
DeveloperExceptionPageMiddleware to capture synchronous and asynchronous
exceptions from the HTTP pipeline and to generate error responses.

ASP.NET Core apps enable the developer exception page by default when both:

Running in the Development environment.


App is using WebApplication.CreateBuilder.

For more information on configuring middleware, see Middleware in Minimal API apps.
Using the preceding Minimal API app, when the Developer Exception Page detects an
unhandled exception, it generates a default plain-text response similar to the following
example:

Console

HTTP/1.1 500 Internal Server Error


Content-Type: text/plain; charset=utf-8
Date: Thu, 27 Oct 2022 18:00:59 GMT
Server: Kestrel
Transfer-Encoding: chunked

System.InvalidOperationException: Sample Exception


at Program.<>c.<<Main>$>b__0_1() in ....:line 17
at lambda_method2(Closure, Object, HttpContext)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext
httpContext)
--- End of stack trace from previous location ---
at
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke
(HttpContext context)
HEADERS
=======
Accept: */*
Connection: keep-alive
Host: localhost:5239
Accept-Encoding: gzip, deflate, br

2 Warning

Don't enable the Developer Exception Page unless the app is running in the
Development environment. Don't share detailed exception information publicly
when the app runs in production. For more information on configuring
environments, see Use multiple environments in ASP.NET Core.

Exception handler
In non-development environments, use the Exception Handler Middleware to produce
an error payload. To configure the Exception Handler Middleware , call
UseExceptionHandler.

For example, the following code changes the app to respond with an RFC 7807 -
compliant payload to the client. For more information, see Problem Details section.

C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseExceptionHandler(exceptionHandlerApp
=> exceptionHandlerApp.Run(async context
=> await Results.Problem()
.ExecuteAsync(context)));

app.Map("/exception", ()
=> { throw new InvalidOperationException("Sample Exception"); });

app.Run();

Client and Server error responses


Consider the following Minimal API app.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

app.Map("/users/{id:int}", (int id)


=> id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)) );

app.Run();

public record User(int Id);

The /users endpoint produces 200 OK with a json representation of User when id is
greater than 0 , otherwise a 400 BAD REQUEST status code without a response body. For
more information about creating a response, see Create responses in Minimal API apps.

The Status Code Pages middleware can be configured to produce a common body
content, when empty, for all HTTP client ( 400 - 499 ) or server ( 500 - 599 ) responses. The
middleware is configured by calling the UseStatusCodePages extension method.

For example, the following example changes the app to respond with an RFC 7807 -
compliant payload to the client for all client and server responses, including routing
errors (for example, 404 NOT FOUND ). For more information, see the Problem Details
section.

C#

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();
app.UseStatusCodePages(async statusCodeContext
=> await Results.Problem(statusCode:
statusCodeContext.HttpContext.Response.StatusCode)
.ExecuteAsync(statusCodeContext.HttpContext));

app.Map("/users/{id:int}", (int id)


=> id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)) );

app.Run();

public record User(int Id);

Problem details
Problem Details are not the only response format to describe an HTTP API error,
however, they are commonly used to report errors for HTTP APIs.

The problem details service implements the IProblemDetailsService interface, which


supports creating problem details in ASP.NET Core. The AddProblemDetails extension
method on IServiceCollection registers the default IProblemDetailsService
implementation.

In ASP.NET Core apps, the following middleware generates problem details HTTP
responses when AddProblemDetails is called, except when the Accept request HTTP
header doesn't include one of the content types supported by the registered
IProblemDetailsWriter (default: application/json ):

ExceptionHandlerMiddleware: Generates a problem details response when a


custom handler is not defined.
StatusCodePagesMiddleware: Generates a problem details response by default.
DeveloperExceptionPageMiddleware: Generates a problem details response in
development when the Accept request HTTP header does not include text/html .

Minimal API apps can be configured to generate problem details response for all HTTP
client and server error responses that don't have a body content yet by using the
AddProblemDetails extension method.

The following code configures the app to generate problem details:

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddProblemDetails();

var app = builder.Build();


app.UseExceptionHandler();
app.UseStatusCodePages();

app.Map("/users/{id:int}", (int id)


=> id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)) );

app.Map("/exception", ()
=> { throw new InvalidOperationException("Sample Exception"); });

app.Run();

For more information on using AddProblemDetails , see Problem Details


Authentication and authorization in
minimal APIs
Article • 06/09/2023

Minimal APIs support all the authentication and authorization options available in
ASP.NET Core and provide some additional functionality to improve the experience
working with authentication.

Key concepts in authentication and


authorization
Authentication is the process of determining a user's identity. Authorization is the
process of determining whether a user has access to a resource. Both authentication and
authorization scenarios share similar implementation semantics in ASP.NET Core.
Authentication is handled by the authentication service, IAuthenticationService, which is
used by authentication middleware. Authorization is handled by the authorization
service, IAuthorizationService, which is used by the authorization middleware.

The authentication service uses registered authentication handlers to complete


authentication-related actions. For example, an authentication-related action is
authenticating a user or signing out a user. Authentication schemes are names that are
used to uniquely identify an authentication handler and its configuration options.
Authentication handlers are responsible for implementing the strategies for
authentication and generating a user's claims given a particular authentication strategy,
such as OAuth or OIDC. The configuration options are unique to the strategy as well and
provide the handler with configuration that affects authentication behavior, such as
redirect URIs.

There are two strategies for determining user access to resources in the authorization
layer:

Role-based strategies determine a user's access based on the role they are
assigned, such as Administrator or User . For more information on role-based
authorization, see role-based authorization documentation.
Claim-based strategies determine a user's access based on claims that are issued
by a central authority. For more information on claim-based authorization, see
claim-based authorization documentation.
In ASP.NET Core, both strategies are captured into an authorization requirement. The
authorization service leverages authorization handlers to determine whether or not a
particular user meets the authorization requirements applied onto a resource.

Enabling authentication in minimal apps


To enable authentication, call AddAuthentication to register the required authentication
services on the app's service provider.

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddAuthentication();
var app = builder.Build();

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


app.Run();

Typically, a specific authentication strategy is used. In the following sample, the app is
configured with support for JWT bearer-based authentication.

C#

var builder = WebApplication.CreateBuilder(args);


// Requires Microsoft.AspNetCore.Authentication.JwtBearer
builder.Services.AddAuthentication().AddJwtBearer();
var app = builder.Build();

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


app.Run();

By default, the WebApplication automatically registers the authentication and


authorization middlewares if certain authentication and authorization services are
enabled. In the following sample, it's not necessary to invoke UseAuthentication or
UseAuthorization to register the middlewares because WebApplication does this
automatically after AddAuthentication or AddAuthorization are called.

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
var app = builder.Build();

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


app.Run();
In some cases, such as controlling middleware order, it's necessary to explicitly register
authentication and authorization. In the following sample, the authentication
middleware runs after the CORS middleware has run. For more information on
middlewares and this automatic behavior, see Middleware in Minimal API apps.

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

var app = builder.Build();

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

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


app.Run();

Configuring authentication strategy


Authentication strategies typically support a variety of configurations that are loaded via
options. Minimal app's support loading options from configuration for the following
authentication strategies:

JWT bearer-based
OpenID Connection-based

The ASP.NET Core framework expects to find these options under the
Authentication:Schemes:{SchemeName} section in configuration. In the following sample,

two different schemes, Bearer and LocalAuthIssuer , are defined with their respective
options. The Authentication:DefaultScheme option can be used to configure the default
authentication strategy that's used.

JSON

{
"Authentication": {
"DefaultScheme": "LocalAuthIssuer",
"Schemes": {
"Bearer": {
"ValidAudiences": [
"https://localhost:7259",
"http://localhost:5259"
],
"ValidIssuer": "dotnet-user-jwts"
},
"LocalAuthIssuer": {
"ValidAudiences": [
"https://localhost:7259",
"http://localhost:5259"
],
"ValidIssuer": "local-auth"
}
}
}
}

In Program.cs , two JWT bearer-based authentication strategies are registered, with the:

"Bearer" scheme name.


"LocalAuthIssuer" scheme name.

"Bearer" is the typical default scheme in JWT-bearer based enabled apps, but the default
scheme can be overridden by setting the DefaultScheme property as in the preceding
example.

The scheme name is used to uniquely identify an authentication strategy and is used as
the lookup key when resolving authentication options from config, as shown in the
following example:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
.AddJwtBearer()
.AddJwtBearer("LocalAuthIssuer");

var app = builder.Build();

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


app.Run();

Configuring authorization policies in minimal


apps
Authentication is used to identify and validate the identity of users against an API.
Authorization is used to validate and verify access to resources in an API and is
facilitated by the IAuthorizationService registered by the AddAuthorization extension
method. In the following scenario, a /hello resource is added that requires a user to
present an admin role claim with a greetings_api scope claim.

Configuring authorization requirements on a resource is a two-step process that


requires:

1. Configuring the authorization requirements in a policy globally.


2. Applying individual policies to resources.

In the following code, AddAuthorizationBuilder is invoked which:

Adds authorization-related services to the DI container.


Returns an AuthorizationBuilder that can be used to directly register authentication
policies.

The code creates a new authorization policy, named admin_greetings , that encapsulates
two authorization requirements:

A role-based requirement via RequireRole for users with an admin role.


A claim-based requirement via RequireClaim that the user must provide a
greetings_api scope claim.

The admin_greetings policy is provided as a required policy to the /hello endpoint.

C#

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorizationBuilder()
.AddPolicy("admin_greetings", policy =>
policy
.RequireRole("admin")
.RequireClaim("scope", "greetings_api"));

var app = builder.Build();

app.MapGet("/hello", () => "Hello world!")


.RequireAuthorization("admin_greetings");

app.Run();

Use dotnet user-jwts for development testing


Throughout this article, an app configured with JWT-bearer based authentication is
used. JWT bearer-based authentication requires that clients present a token in the
request header to validate their identity and claims. Typically, these tokens are issued by
a central authority, such as an identity server.

When developing on the local machine, the dotnet user-jwts tool can be used to create
bearer tokens.

.NET CLI

dotnet user-jwts create

7 Note

When invoked on a project, the tool automatically adds the authentication options
matching the generated token to appsettings.json .

Tokens can be configured with a variety of customizations. For example, to create a


token for the admin role and greetings_api scope expected by the authorization policy
in the preceding code:

.NET CLI

dotnet user-jwts create --scope "greetings_api" --role "admin"

The generated token can then be sent as part of the header in the testing tool of choice.
For example, with curl:

.NET CLI

curl -i -H "Authorization: Bearer {token}" https://localhost:{port}/hello

For more information on the dotnet user-jwts tool, read the complete documentation.
Overview of ASP.NET Core SignalR
Article • 02/14/2023

What is SignalR?
ASP.NET Core SignalR is an open-source library that simplifies adding real-time web
functionality to apps. Real-time web functionality enables server-side code to push
content to clients instantly.

Good candidates for SignalR:

Apps that require high frequency updates from the server. Examples are gaming,
social networks, voting, auction, maps, and GPS apps.
Dashboards and monitoring apps. Examples include company dashboards, instant
sales updates, or travel alerts.
Collaborative apps. Whiteboard apps and team meeting software are examples of
collaborative apps.
Apps that require notifications. Social networks, email, chat, games, travel alerts,
and many other apps use notifications.

SignalR provides an API for creating server-to-client remote procedure calls (RPC) . The
RPCs invoke functions on clients from server-side .NET Core code. There are several
supported platforms, each with their respective client SDK. Because of this, the
programming language being invoked by the RPC call varies.

Here are some features of SignalR for ASP.NET Core:

Handles connection management automatically.


Sends messages to all connected clients simultaneously. For example, a chat room.
Sends messages to specific clients or groups of clients.
Scales to handle increasing traffic.
SignalR Hub Protocol

The source is hosted in a SignalR repository on GitHub .

Transports
SignalR supports the following techniques for handling real-time communication (in
order of graceful fallback):

WebSockets
Server-Sent Events
Long Polling

SignalR automatically chooses the best transport method that is within the capabilities
of the server and client.

Hubs
SignalR uses hubs to communicate between clients and servers.

A hub is a high-level pipeline that allows a client and server to call methods on each
other. SignalR handles the dispatching across machine boundaries automatically,
allowing clients to call methods on the server and vice versa. You can pass strongly-
typed parameters to methods, which enables model binding. SignalR provides two built-
in hub protocols: a text protocol based on JSON and a binary protocol based on
MessagePack . MessagePack generally creates smaller messages compared to JSON.
Older browsers must support XHR level 2 to provide MessagePack protocol support.

Hubs call client-side code by sending messages that contain the name and parameters
of the client-side method. Objects sent as method parameters are deserialized using the
configured protocol. The client tries to match the name to a method in the client-side
code. When the client finds a match, it calls the method and passes to it the deserialized
parameter data.

Browsers that don't support ECMAScript 6


(ES6)
SignalR targets ES6. For browsers that don't support ES6, transpile the library to ES5. For
more information, see Getting Started with ES6 – Transpiling ES6 to ES5 with Traceur and
Babel .

Additional resources
Introduction to ASP.NET Core SignalR
Get started with SignalR for ASP.NET Core
Supported Platforms
Hubs
JavaScript client
Browsers that don't support ECMAScript 6 (ES6)
ASP.NET Core SignalR supported
platforms
Article • 02/14/2023

Server system requirements


SignalR for ASP.NET Core supports any server platform that ASP.NET Core supports.

JavaScript client
The JavaScript client runs on the current Node.js long-term support (LTS) release and
the following browsers:

Browser Version

Apple Safari, including iOS Current†

Google Chrome, including Android Current†

Microsoft Edge Current†

Mozilla Firefox Current†

†Current refers to the latest version of the browser.

The JavaScript client doesn't support Internet Explorer and other older browsers. The
client might have unexpected behavior and errors on unsupported browsers.

.NET client
The .NET client runs on any platform supported by ASP.NET Core. For example, Xamarin
developers can use SignalR for building Android apps using Xamarin.Android 8.4.0.1
and later and iOS apps using Xamarin.iOS 11.14.0.4 and later.

If the server runs IIS, the WebSockets transport requires IIS 8.0 or later on Windows
Server 2012 or later. Other transports are supported on all platforms.

Java client
The Java client supports Java 8 and later versions.
Unsupported clients
The following clients are available but are experimental or unofficial. The following
clients aren't currently supported and may never be supported:

C++ client
Swift client

Browsers that don't support ECMAScript 6


(ES6)
SignalR targets ES6. For browsers that don't support ES6, transpile the library to ES5. For
more information, see Getting Started with ES6 – Transpiling ES6 to ES5 with Traceur and
Babel .
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)
Use hubs in SignalR for ASP.NET Core
Article • 02/24/2023

By Rachel Appel and Kevin Griffin

The SignalR Hubs API enables connected clients to call methods on the server. The
server defines methods that are called from the client and the client defines methods
that are called from the server. SignalR takes care of everything required to make real-
time client-to-server and server-to-client communication possible.

Configure SignalR hubs


To register the services required by SignalR hubs, call AddSignalR in Program.cs :

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

To configure SignalR endpoints, call MapHub, also in Program.cs :

C#

app.MapRazorPages();
app.MapHub<ChatHub>("/Chat");

app.Run();

7 Note

ASP.NET Core SignalR server-side assemblies are now installed with the .NET Core
SDK. See SignalR assemblies in shared framework for more information.

Create and use hubs


Create a hub by declaring a class that inherits from Hub. Add public methods to the
class to make them callable from clients:

C#
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
=> await Clients.All.SendAsync("ReceiveMessage", user, message);
}

7 Note

Hubs are transient:

Don't store state in a property of the hub class. Each hub method call is
executed on a new hub instance.
Don't instantiate a hub directly via dependency injection. To send messages to
a client from elsewhere in your application use an IHubContext.
Use await when calling asynchronous methods that depend on the hub
staying alive. For example, a method such as Clients.All.SendAsync(...) can
fail if it's called without await and the hub method completes before
SendAsync finishes.

The Context object


The Hub class includes a Context property that contains the following properties with
information about the connection:

Property Description

ConnectionId Gets the unique ID for the connection, assigned by SignalR. There's one
connection ID for each connection.

UserIdentifier Gets the user identifier. By default, SignalR uses the


ClaimTypes.NameIdentifier from the ClaimsPrincipal associated with the
connection as the user identifier.

User Gets the ClaimsPrincipal associated with the current user.

Items Gets a key/value collection that can be used to share data within the scope
of this connection. Data can be stored in this collection and it will persist for
the connection across different hub method invocations.

Features Gets the collection of features available on the connection. For now, this
collection isn't needed in most scenarios, so it isn't documented in detail
yet.
Property Description

ConnectionAborted Gets a CancellationToken that notifies when the connection is aborted.

Hub.Context also contains the following methods:

Method Description

GetHttpContext Returns the HttpContext for the connection, or null if the connection isn't
associated with an HTTP request. For HTTP connections, use this method to get
information such as HTTP headers and query strings.

Abort Aborts the connection.

The Clients object


The Hub class includes a Clients property that contains the following properties for
communication between server and client:

Property Description

All Calls a method on all connected clients

Caller Calls a method on the client that invoked the hub method

Others Calls a method on all connected clients except the client that invoked the method

Hub.Clients also contains the following methods:

Method Description

AllExcept Calls a method on all connected clients except for the specified connections

Client Calls a method on a specific connected client

Clients Calls a method on specific connected clients

Group Calls a method on all connections in the specified group

GroupExcept Calls a method on all connections in the specified group, except the specified
connections

Groups Calls a method on multiple groups of connections

OthersInGroup Calls a method on a group of connections, excluding the client that invoked the
hub method

User Calls a method on all connections associated with a specific user


Method Description

Users Calls a method on all connections associated with the specified users

Each property or method in the preceding tables returns an object with a SendAsync
method. The SendAsync method receives the name of the client method to call and any
parameters.

The object returned by the Client and Caller methods also contain an InvokeAsync
method, which can be used to wait for a result from the client.

Send messages to clients


To make calls to specific clients, use the properties of the Clients object. In the
following example, there are three hub methods:

SendMessage sends a message to all connected clients, using Clients.All .

SendMessageToCaller sends a message back to the caller, using Clients.Caller .

SendMessageToGroup sends a message to all clients in the SignalR Users group.

C#

public async Task SendMessage(string user, string message)


=> await Clients.All.SendAsync("ReceiveMessage", user, message);

public async Task SendMessageToCaller(string user, string message)


=> await Clients.Caller.SendAsync("ReceiveMessage", user, message);

public async Task SendMessageToGroup(string user, string message)


=> await Clients.Group("SignalR Users").SendAsync("ReceiveMessage",
user, message);

Strongly typed hubs


A drawback of using SendAsync is that it relies on a string to specify the client method to
be called. This leaves code open to runtime errors if the method name is misspelled or
missing from the client.

An alternative to using SendAsync is to strongly type the Hub class with Hub<T>. In the
following example, the ChatHub client method has been extracted out into an interface
called IChatClient :

C#
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
}

This interface can be used to refactor the preceding ChatHub example to be strongly
typed:

C#

public class StronglyTypedChatHub : Hub<IChatClient>


{
public async Task SendMessage(string user, string message)
=> await Clients.All.ReceiveMessage(user, message);

public async Task SendMessageToCaller(string user, string message)


=> await Clients.Caller.ReceiveMessage(user, message);

public async Task SendMessageToGroup(string user, string message)


=> await Clients.Group("SignalR Users").ReceiveMessage(user,
message);
}

Using Hub<IChatClient> enables compile-time checking of the client methods. This


prevents issues caused by using strings, since Hub<T> can only provide access to the
methods defined in the interface. Using a strongly typed Hub<T> disables the ability to
use SendAsync .

7 Note

The Async suffix isn't stripped from method names. Unless a client method is
defined with .on('MyMethodAsync') , don't use MyMethodAsync as the name.

Client results
In addition to making calls to clients, the server can request a result from a client. This
requires the server to use ISingleClientProxy.InvokeAsync and the client to return a
result from its .On handler.

There are two ways to use the API on the server, the first is to call Client(...) or
Caller on the Clients property in a Hub method:

C#
public class ChatHub : Hub
{
public async Task<string> WaitForMessage(string connectionId)
{
var message = await Clients.Client(connectionId).InvokeAsync<string>
(
"GetMessage");
return message;
}
}

The second way is to call Client(...) on an instance of IHubContext<T>:

C#

async Task SomeMethod(IHubContext<MyHub> context)


{
string result = await
context.Clients.Client(connectionID).InvokeAsync<string>(
"GetMessage");
}

Strongly-typed hubs can also return values from interface methods:

C#

public interface IClient


{
Task<string> GetMessage();
}

public class ChatHub : Hub<IClient>


{
public async Task<string> WaitForMessage(string connectionId)
{
string message = await Clients.Client(connectionId).GetMessage();
return message;
}
}

Clients return results in their .On(...) handlers, as shown below:

.NET client

C#

hubConnection.On("GetMessage", async () =>


{
Console.WriteLine("Enter message:");
var message = await Console.In.ReadLineAsync();
return message;
});

Typescript client

TypeScript

hubConnection.on("GetMessage", async () => {


let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("message");
}, 100);
});
return promise;
});

Java client

Java

hubConnection.onWithResult("GetMessage", () -> {
return Single.just("message");
});

Change the name of a hub method


By default, a server hub method name is the name of the .NET method. To change this
default behavior for a specific method, use the HubMethodName attribute. The client
should use this name instead of the .NET method name when invoking the method:

C#

[HubMethodName("SendMessageToUser")]
public async Task DirectMessage(string user, string message)
=> await Clients.User(user).SendAsync("ReceiveMessage", user, message);

Inject services into a hub


Hub constructors can accept services from DI as parameters, which can be stored in
properties on the class for use in a hub method.
When injecting multiple services for different hub methods or as an alternative way of
writing code, hub methods can also accept services from DI. By default, hub method
parameters are inspected and resolved from DI if possible.

C#

services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();

// ...

public class ChatHub : Hub


{
public Task SendMessage(string user, string message, IDatabaseService
dbService)
{
var userName = dbService.GetUserName(user);
return Clients.All.SendAsync("ReceiveMessage", userName, message);
}
}

If implicit resolution of parameters from services isn't desired, disable it with


DisableImplicitFromServicesParameters. To explicitly specify which parameters are
resolved from DI in hub methods, use the DisableImplicitFromServicesParameters option
and use the [FromServices] attribute or a custom attribute that implements
IFromServiceMetadata on the hub method parameters that should be resolved from DI.

C#

services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();
services.AddSignalR(options =>
{
options.DisableImplicitFromServicesParameters = true;
});

// ...

public class ChatHub : Hub


{
public Task SendMessage(string user, string message,
[FromServices] IDatabaseService dbService)
{
var userName = dbService.GetUserName(user);
return Clients.All.SendAsync("ReceiveMessage", userName, message);
}
}

7 Note
This feature makes use of IServiceProviderIsService, which is optionally
implemented by DI implementations. If the app's DI container doesn't support this
feature, injecting services into hub methods isn't supported.

Handle events for a connection


The SignalR Hubs API provides the OnConnectedAsync and OnDisconnectedAsync
virtual methods to manage and track connections. Override the OnConnectedAsync virtual
method to perform actions when a client connects to the hub, such as adding it to a
group:

C#

public override async Task OnConnectedAsync()


{
await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnConnectedAsync();
}

Override the OnDisconnectedAsync virtual method to perform actions when a client


disconnects. If the client disconnects intentionally, such as by calling connection.stop() ,
the exception parameter is set to null . However, if the client disconnects due to an
error, such as a network failure, the exception parameter contains an exception that
describes the failure:

C#

public override async Task OnDisconnectedAsync(Exception? exception)


{
await base.OnDisconnectedAsync(exception);
}

RemoveFromGroupAsync does not need to be called in OnDisconnectedAsync, it's


automatically handled for you.

Handle errors
Exceptions thrown in hub methods are sent to the client that invoked the method. On
the JavaScript client, the invoke method returns a JavaScript Promise . Clients can
attach a catch handler to the returned promise or use try / catch with async / await to
handle exceptions:
JavaScript

try {
await connection.invoke("SendMessage", user, message);
} catch (err) {
console.error(err);
}

Connections aren't closed when a hub throws an exception. By default, SignalR returns a
generic error message to the client, as shown in the following example:

text

Microsoft.AspNetCore.SignalR.HubException: An unexpected error occurred


invoking 'SendMessage' on the server.

Unexpected exceptions often contain sensitive information, such as the name of a


database server in an exception triggered when the database connection fails. SignalR
doesn't expose these detailed error messages by default as a security measure. For more
information on why exception details are suppressed, see Security considerations in
ASP.NET Core SignalR.

If an exceptional condition must be propagated to the client, use the HubException


class. If a HubException is thrown in a hub method, SignalR sends the entire exception
message to the client, unmodified:

C#

public Task ThrowException()


=> throw new HubException("This error will be sent to the client!");

7 Note

SignalR only sends the Message property of the exception to the client. The stack
trace and other properties on the exception aren't available to the client.

Additional resources
View or download sample code (how to download)
Overview of ASP.NET Core SignalR
ASP.NET Core SignalR JavaScript client
Publish an ASP.NET Core SignalR app to Azure App Service
Send messages from outside a hub
Article • 02/24/2023

The SignalR hub is the core abstraction for sending messages to clients connected to
the SignalR server. It's also possible to send messages from other places in your app
using the IHubContext service. This article explains how to access a SignalR IHubContext
to send notifications to clients from outside a hub.

7 Note

The IHubContext is for sending notifications to clients, it is not used to call methods
on the Hub .

View or download sample code (how to download)

Get an instance of IHubContext


In ASP.NET Core SignalR, you can access an instance of IHubContext via dependency
injection. You can inject an instance of IHubContext into a controller, middleware, or
other DI service. Use the instance to send messages to clients.

7 Note

This differs from ASP.NET 4.x SignalR which used GlobalHost to provide access to
the IHubContext . ASP.NET Core has a dependency injection framework that
removes the need for this global singleton.

Inject an instance of IHubContext in a controller


You can inject an instance of IHubContext into a controller by adding it to your
constructor:

C#

public class HomeController : Controller


{
private readonly IHubContext<NotificationHub> _hubContext;

public HomeController(IHubContext<NotificationHub> hubContext)


{
_hubContext = hubContext;
}
}

With access to an instance of IHubContext , call client methods as if you were in the hub
itself:

C#

public async Task<IActionResult> Index()


{
await _hubContext.Clients.All.SendAsync("Notify", $"Home page loaded at:
{DateTime.Now}");
return View();
}

Get an instance of IHubContext in middleware


Access the IHubContext within the middleware pipeline like so:

C#

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


{
var hubContext = context.RequestServices
.GetRequiredService<IHubContext<ChatHub>>();
//...

if (next != null)
{
await next.Invoke();
}
});

7 Note

When client methods are called from outside of the Hub class, there's no caller
associated with the invocation. Therefore, there's no access to the ConnectionId ,
Caller , and Others properties.

Apps that need to map a user to the connection ID and persist that mapping can
do one of the following:

Persist mapping of single or multiple connections as groups. See Groups in


SignalR for more information.
Retain connection and user information through a singleton service. See
Inject services into a hub for more information. The singleton service can use
any storage method, such as:
In-memory storage in a dictionary.
Permanent external storage. For example, a database or Azure Table
storage using the Azure.Data.Tables NuGet package .
Pass the connection ID between clients.

Get an instance of IHubContext from IHost


Accessing an IHubContext from the web host is useful for integrating with areas outside
of ASP.NET Core, for example, using third-party dependency injection frameworks:

C#

public class Program


{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
var hubContext =
host.Services.GetService(typeof(IHubContext<ChatHub>));
host.Run();
}

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


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

Inject a strongly-typed HubContext


To inject a strongly-typed HubContext, ensure your Hub inherits from Hub<T> . Inject it
using the IHubContext<THub, T> interface rather than IHubContext<THub> .

C#

public class ChatController : Controller


{
public IHubContext<ChatHub, IChatClient> _strongChatHubContext { get; }

public ChatController(IHubContext<ChatHub, IChatClient> chatHubContext)


{
_strongChatHubContext = chatHubContext;
}

public async Task SendMessage(string user, string message)


{
await _strongChatHubContext.Clients.All.ReceiveMessage(user,
message);
}
}

See Strongly typed hubs for more information.

Use IHubContext in generic code


An injected IHubContext<THub> instance can be cast to IHubContext without a generic
Hub type specified.

C#

class MyHub : Hub


{ }

class MyOtherHub : Hub


{ }

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


{
var myHubContext = context.RequestServices
.GetRequiredService<IHubContext<MyHub>>();
var myOtherHubContext = context.RequestServices
.GetRequiredService<IHubContext<MyOtherHub>>();
await CommonHubContextMethod((IHubContext)myHubContext);
await CommonHubContextMethod((IHubContext)myOtherHubContext);

await next.Invoke();
}

async Task CommonHubContextMethod(IHubContext context)


{
await context.Clients.All.SendAsync("clientMethod", new Args());
}

This is useful when:

Writing libraries that don't have a reference to the specific Hub type the app is
using.
Writing code that is generic and can apply to multiple different Hub
implementations
Additional resources
SignalR assemblies in shared framework
Get started with ASP.NET Core SignalR
Use hubs in ASP.NET Core SignalR
Publish an ASP.NET Core SignalR app to Azure App Service
Manage users and groups in SignalR
Article • 06/03/2022

By Brennan Conroy

SignalR allows messages to be sent to all connections associated with a specific user, as
well as to named groups of connections.

View or download sample code (how to download)

Users in SignalR
A single user in SignalR can have multiple connections to an app. For example, a user
could be connected on their desktop as well as their phone. Each device has a separate
SignalR connection, but they're all associated with the same user. If a message is sent to
the user, all of the connections associated with that user receive the message. The user
identifier for a connection can be accessed by the Context.UserIdentifier property in
the hub.

By default, SignalR uses the ClaimTypes.NameIdentifier from the ClaimsPrincipal


associated with the connection as the user identifier. To customize this behavior, see Use
claims to customize identity handling.

Send a message to a specific user by passing the user identifier to the User function in a
hub method, as shown in the following example:

7 Note

The user identifier is case-sensitive.

C#

public Task SendPrivateMessage(string user, string message)


{
return Clients.User(user).SendAsync("ReceiveMessage", message);
}

Groups in SignalR
A group is a collection of connections associated with a name. Messages can be sent to
all connections in a group. Groups are the recommended way to send to a connection
or multiple connections because the groups are managed by the application. A
connection can be a member of multiple groups. Groups are ideal for something like a
chat application, where each room can be represented as a group. Connections are
added to or removed from groups via the AddToGroupAsync and RemoveFromGroupAsync
methods.

C#

public async Task AddToGroup(string groupName)


{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);

await Clients.Group(groupName).SendAsync("Send", $"


{Context.ConnectionId} has joined the group {groupName}.");
}

public async Task RemoveFromGroup(string groupName)


{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);

await Clients.Group(groupName).SendAsync("Send", $"


{Context.ConnectionId} has left the group {groupName}.");
}

Group membership isn't preserved when a connection reconnects. The connection


needs to rejoin the group when it's re-established. It's not possible to count the
members of a group, since this information is not available if the application is scaled to
multiple servers.

To protect access to resources while using groups, use authentication and authorization
functionality in ASP.NET Core. If a user is added to a group only when the credentials
are valid for that group, messages sent to that group will only go to authorized users.
However, groups are not a security feature. Authentication claims have features that
groups do not, such as expiry and revocation. If a user's permission to access the group
is revoked, the app must remove the user from the group explicitly.

7 Note

Group names are case-sensitive.

Additional resources
Get started with ASP.NET Core SignalR
Use hubs in ASP.NET Core SignalR
Publish an ASP.NET Core SignalR app to Azure App Service
SignalR API design considerations
Article • 02/24/2023

By Andrew Stanton-Nurse

This article provides guidance for building SignalR-based APIs.

Use custom object parameters to ensure


backwards-compatibility
Adding parameters to a SignalR hub method (on either the client or the server) is a
breaking change. This means older clients/servers will get errors when they try to invoke
the method without the appropriate number of parameters. However, adding properties
to a custom object parameter is not a breaking change. This can be used to design
compatible APIs that are resilient to changes on the client or the server.

For example, consider a server-side API like the following:

C#

public async Task<string> GetTotalLength(string param1)


{
return param1.Length;
}

The JavaScript client calls this method using invoke as follows:

TypeScript

connection.invoke("GetTotalLength", "value1");

If you later add a second parameter to the server method, older clients won't provide
this parameter value. For example:

C#

public async Task<string> GetTotalLength(string param1, string param2)


{
return param1.Length + param2.Length;
}

When the old client tries to invoke this method, it will get an error like this:
Microsoft.AspNetCore.SignalR.HubException: Failed to invoke 'GetTotalLength'
due to an error on the server.

On the server, you'll see a log message like this:

System.IO.InvalidDataException: Invocation provides 1 argument(s) but target


expects 2.

The old client only sent one parameter, but the newer server API required two
parameters. Using custom objects as parameters gives you more flexibility. Let's
redesign the original API to use a custom object:

C#

public class TotalLengthRequest


{
public string Param1 { get; set; }
}

public async Task GetTotalLength(TotalLengthRequest req)


{
return req.Param1.Length;
}

Now, the client uses an object to call the method:

TypeScript

connection.invoke("GetTotalLength", { param1: "value1" });

Instead of adding a parameter, add a property to the TotalLengthRequest object:

C#

public class TotalLengthRequest


{
public string Param1 { get; set; }
public string Param2 { get; set; }
}

public async Task GetTotalLength(TotalLengthRequest req)


{
var length = req.Param1.Length;
if (req.Param2 != null)
{
length += req.Param2.Length;
}
return length;
}

When the old client sends a single parameter, the extra Param2 property will be left
null . You can detect a message sent by an older client by checking the Param2 for null

and apply a default value. A new client can send both parameters.

TypeScript

connection.invoke("GetTotalLength", { param1: "value1", param2: "value2" });

The same technique works for methods defined on the client. You can send a custom
object from the server side:

C#

public async Task Broadcast(string message)


{
await Clients.All.SendAsync("ReceiveMessage", new
{
Message = message
});
}

On the client side, you access the Message property rather than using a parameter:

TypeScript

connection.on("ReceiveMessage", (req) => {


appendMessageToChatWindow(req.message);
});

If you later decide to add the sender of the message to the payload, add a property to
the object:

C#

public async Task Broadcast(string message)


{
await Clients.All.SendAsync("ReceiveMessage", new
{
Sender = Context.User.Identity.Name,
Message = message
});
}

The older clients won't be expecting the Sender value, so they'll ignore it. A new client
can accept it by updating to read the new property:

TypeScript

connection.on("ReceiveMessage", (req) => {


let message = req.message;
if (req.sender) {
message = req.sender + ": " + message;
}
appendMessageToChatWindow(message);
});

In this case, the new client is also tolerant of an old server that doesn't provide the
Sender value. Since the old server won't provide the Sender value, the client checks to
see if it exists before accessing it.

Additional resources
SignalR assemblies in shared framework
Use hub filters in ASP.NET Core SignalR
Article • 07/03/2022

Hub filters:

Are available in ASP.NET Core 5.0 or later.


Allow logic to run before and after hub methods are invoked by clients.

This article provides guidance for writing and using hub filters.

Configure hub filters


Hub filters can be applied globally or per hub type. The order in which filters are added
is the order in which the filters run. Global hub filters run before local hub filters.

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddSignalR(options =>
{
// Global filters will run first
options.AddFilter<CustomFilter>();
}).AddHubOptions<ChatHub>(options =>
{
// Local filters will run second
options.AddFilter<CustomFilter2>();
});
}

A hub filter can be added in one of the following ways:

Add a filter by concrete type:

C#

hubOptions.AddFilter<TFilter>();

This will be resolved from dependency injection (DI) or type activated.

Add a filter by runtime type:

C#

hubOptions.AddFilter(typeof(TFilter));
This will be resolved from DI or type activated.

Add a filter by instance:

C#

hubOptions.AddFilter(new MyFilter());

This instance will be used like a singleton. All hub method invocations will use the
same instance.

Hub filters are created and disposed per hub invocation. If you want to store global
state in the filter, or no state, add the hub filter type to DI as a singleton for better
performance. Alternatively, add the filter as an instance if you can.

Create hub filters


Create a filter by declaring a class that inherits from IHubFilter , and add the
InvokeMethodAsync method. There is also OnConnectedAsync and OnDisconnectedAsync

that can optionally be implemented to wrap the OnConnectedAsync and


OnDisconnectedAsync hub methods respectively.

C#

public class CustomFilter : IHubFilter


{
public async ValueTask<object> InvokeMethodAsync(
HubInvocationContext invocationContext, Func<HubInvocationContext,
ValueTask<object>> next)
{
Console.WriteLine($"Calling hub method
'{invocationContext.HubMethodName}'");
try
{
return await next(invocationContext);
}
catch (Exception ex)
{
Console.WriteLine($"Exception calling
'{invocationContext.HubMethodName}': {ex}");
throw;
}
}

// Optional method
public Task OnConnectedAsync(HubLifetimeContext context,
Func<HubLifetimeContext, Task> next)
{
return next(context);
}

// Optional method
public Task OnDisconnectedAsync(
HubLifetimeContext context, Exception exception,
Func<HubLifetimeContext, Exception, Task> next)
{
return next(context, exception);
}
}

Filters are very similar to middleware. The next method invokes the next filter. The final
filter will invoke the hub method. Filters can also store the result from awaiting next and
run logic after the hub method has been called before returning the result from next .

To skip a hub method invocation in a filter, throw an exception of type HubException


instead of calling next . The client will receive an error if it was expecting a result.

Use hub filters


When writing the filter logic, try to make it generic by using attributes on hub methods
instead of checking for hub method names.

Consider a filter that will check a hub method argument for banned phrases and replace
any phrases it finds with *** . For this example, assume a LanguageFilterAttribute class
is defined. The class has a property named FilterArgument that can be set when using
the attribute.

1. Place the attribute on the hub method that has a string argument to be cleaned:

C#

public class ChatHub


{
[LanguageFilter(filterArgument = 0)]
public async Task SendMessage(string message, string username)
{
await Clients.All.SendAsync("SendMessage", $"{username} says:
{message}");
}
}

2. Define a hub filter to check for the attribute and replace banned phrases in a hub
method argument with *** :
C#

public class LanguageFilter : IHubFilter


{
// populated from a file or inline
private List<string> bannedPhrases = new List<string> { "async
void", ".Result" };

public async ValueTask<object>


InvokeMethodAsync(HubInvocationContext invocationContext,
Func<HubInvocationContext, ValueTask<object>> next)
{
var languageFilter =
(LanguageFilterAttribute)Attribute.GetCustomAttribute(
invocationContext.HubMethod,
typeof(LanguageFilterAttribute));
if (languageFilter != null &&
invocationContext.HubMethodArguments.Count >
languageFilter.FilterArgument &&

invocationContext.HubMethodArguments[languageFilter.FilterArgument] is
string str)
{
foreach (var bannedPhrase in bannedPhrases)
{
str = str.Replace(bannedPhrase, "***");
}

var arguments =
invocationContext.HubMethodArguments.ToArray();
arguments[languageFilter.FilterArgument] = str;
invocationContext = new
HubInvocationContext(invocationContext.Context,
invocationContext.ServiceProvider,
invocationContext.Hub,
invocationContext.HubMethod,
arguments);
}

return await next(invocationContext);


}
}

3. Register the hub filter in the Startup.ConfigureServices method. To avoid


reinitializing the banned phrases list for every invocation, the hub filter is
registered as a singleton:

C#

public void ConfigureServices(IServiceCollection services)


{
services.AddSignalR(hubOptions =>
{
hubOptions.AddFilter<LanguageFilter>();
});

services.AddSingleton<LanguageFilter>();
}

The HubInvocationContext object


The HubInvocationContext contains information for the current hub method invocation.

Property Description Type

Context The HubCallerContext contains information about HubCallerContext


the connection.

Hub The instance of the Hub being used for this hub Hub
method invocation.

HubMethodName The name of the hub method being invoked. string

HubMethodArguments The list of arguments being passed to the hub IReadOnlyList<string>


method.

ServiceProvider The scoped service provider for this hub method IServiceProvider
invocation.

HubMethod The hub method information. MethodInfo

The HubLifetimeContext object


The HubLifetimeContext contains information for the OnConnectedAsync and
OnDisconnectedAsync hub methods.

Property Description Type

Context The HubCallerContext contains information about the HubCallerContext


connection.

Hub The instance of the Hub being used for this hub method Hub
invocation.

ServiceProvider The scoped service provider for this hub method IServiceProvider
invocation.
Authorization and filters
Authorize attributes on hub methods run before hub filters.
ASP.NET Core SignalR clients
Article • 02/14/2023

Versioning, support, and compatibility


The SignalR clients ship alongside the server components and are versioned to match.
Any supported client can safely connect to any supported server, and any compatibility
issues would be considered bugs to be fixed. SignalR clients are supported in the same
support lifecycle as the rest of .NET Core. See the .NET Core Support Policy for details.

Many features require a compatible client and server. See below for a table showing the
minimum versions for various features.

The 1.x versions of SignalR map to the 2.1 and 2.2 .NET Core releases and have the same
lifetime. For version 3.x and above, the SignalR version exactly matches the rest of .NET
and has the same support lifecycle.

SignalR .NET Core version Support level End of support


version

1.0.x 2.1.x Long Term Support August 21, 2021

1.1.x 2.2.x End Of Life December 23,


2019

3.x or higher same as SignalR See the the .NET Core Support
version Policy

NOTE: In ASP.NET Core 3.0, the JavaScript client moved to the @microsoft/signalr npm
package.

Feature distribution
The table below shows the features and support for the clients that offer real-time
support. For each feature, the minimum version supporting this feature is listed. If no
version is listed, the feature isn't supported.

Feature Server .NET JavaScript Java client


client client

Azure SignalR Service Support 2.1.0 1.0.0 1.0.0 1.0.0

Server-to-client Streaming 2.1.0 1.0.0 1.0.0 1.0.0


Feature Server .NET JavaScript Java client
client client

Client-to-server Streaming 3.0.0 3.0.0 3.0.0 3.0.0

Automatic Reconnection (.NET, 3.0.0 3.0.0 3.0.0 ❌


JavaScript)

WebSockets Transport 2.1.0 1.0.0 1.0.0 1.0.0

Server-Sent Events Transport 2.1.0 1.0.0 1.0.0 ❌

Long Polling Transport 2.1.0 1.0.0 1.0.0 3.0.0

JSON Hub Protocol 2.1.0 1.0.0 1.0.0 1.0.0

MessagePack Hub Protocol 2.1.0 1.0.0 1.0.0 5.0.0

Client Results 7.0.0 7.0.0 7.0.0 7.0.0

Support for enabling additional client features is tracked in our issue tracker .

Browsers that don't support ECMAScript 6


(ES6)
SignalR targets ES6. For browsers that don't support ES6, transpile the library to ES5. For
more information, see Getting Started with ES6 – Transpiling ES6 to ES5 with Traceur and
Babel .

Additional resources
Get started with SignalR for ASP.NET Core
Supported platforms
Hubs
JavaScript client
ASP.NET Core SignalR .NET Client
Article • 06/03/2022

The ASP.NET Core SignalR .NET client library lets you communicate with SignalR hubs
from .NET apps.

View or download sample code (how to download)

The code sample in this article is a WPF app that uses the ASP.NET Core SignalR .NET
client.

Install the SignalR .NET client package


The Microsoft.AspNetCore.SignalR.Client package is required for .NET clients to
connect to SignalR hubs.

Visual Studio

To install the client library, run the following command in the Package Manager
Console window:

PowerShell

Install-Package Microsoft.AspNetCore.SignalR.Client

Connect to a hub
To establish a connection, create a HubConnectionBuilder and call Build . The hub URL,
protocol, transport type, log level, headers, and other options can be configured while
building a connection. Configure any required options by inserting any of the
HubConnectionBuilder methods into Build . Start the connection with StartAsync .

C#

using System;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.AspNetCore.SignalR.Client;

namespace SignalRChatClient
{
public partial class MainWindow : Window
{
HubConnection connection;
public MainWindow()
{
InitializeComponent();

connection = new HubConnectionBuilder()


.WithUrl("http://localhost:53353/ChatHub")
.Build();

connection.Closed += async (error) =>


{
await Task.Delay(new Random().Next(0,5) * 1000);
await connection.StartAsync();
};
}

private async void connectButton_Click(object sender,


RoutedEventArgs e)
{
connection.On<string, string>("ReceiveMessage", (user, message)
=>
{
this.Dispatcher.Invoke(() =>
{
var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});

try
{
await connection.StartAsync();
messagesList.Items.Add("Connection started");
connectButton.IsEnabled = false;
sendButton.IsEnabled = true;
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}

private async void sendButton_Click(object sender, RoutedEventArgs


e)
{
try
{
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}
}
}

Handle lost connection

Automatically reconnect
The HubConnection can be configured to automatically reconnect using the
WithAutomaticReconnect method on the HubConnectionBuilder. It won't automatically
reconnect by default.

C#

HubConnection connection= new HubConnectionBuilder()


.WithUrl(new Uri("http://127.0.0.1:5000/chathub"))
.WithAutomaticReconnect()
.Build();

Without any parameters, WithAutomaticReconnect() configures the client to wait 0, 2, 10,


and 30 seconds respectively before trying each reconnect attempt, stopping after four
failed attempts.

Before starting any reconnect attempts, the HubConnection will transition to the
HubConnectionState.Reconnecting state and fire the Reconnecting event. This provides

an opportunity to warn users that the connection has been lost and to disable UI
elements. Non-interactive apps can start queuing or dropping messages.

C#

connection.Reconnecting += error =>


{
Debug.Assert(connection.State == HubConnectionState.Reconnecting);

// Notify users the connection was lost and the client is reconnecting.
// Start queuing or dropping messages.

return Task.CompletedTask;
};

If the client successfully reconnects within its first four attempts, the HubConnection will
transition back to the Connected state and fire the Reconnected event. This provides an
opportunity to inform users the connection has been reestablished and dequeue any
queued messages.

Since the connection looks entirely new to the server, a new ConnectionId will be
provided to the Reconnected event handlers.

2 Warning

The Reconnected event handler's connectionId parameter will be null if the


HubConnection was configured to skip negotiation.

C#

connection.Reconnected += connectionId =>


{
Debug.Assert(connection.State == HubConnectionState.Connected);

// Notify users the connection was reestablished.


// Start dequeuing messages queued while reconnecting if any.

return Task.CompletedTask;
};

WithAutomaticReconnect() won't configure the HubConnection to retry initial start

failures, so start failures need to be handled manually:

C#

public static async Task<bool> ConnectWithRetryAsync(HubConnection


connection, CancellationToken token)
{
// Keep trying to until we can start or the token is canceled.
while (true)
{
try
{
await connection.StartAsync(token);
Debug.Assert(connection.State == HubConnectionState.Connected);
return true;
}
catch when (token.IsCancellationRequested)
{
return false;
}
catch
{
// Failed to connect, trying again in 5000 ms.
Debug.Assert(connection.State ==
HubConnectionState.Disconnected);
await Task.Delay(5000);
}
}
}

If the client doesn't successfully reconnect within its first four attempts, the
HubConnection will transition to the Disconnected state and fire the Closed event. This
provides an opportunity to attempt to restart the connection manually or inform users
the connection has been permanently lost.

C#

connection.Closed += error =>


{
Debug.Assert(connection.State == HubConnectionState.Disconnected);

// Notify users the connection has been closed or manually try to


restart the connection.

return Task.CompletedTask;
};

In order to configure a custom number of reconnect attempts before disconnecting or


change the reconnect timing, WithAutomaticReconnect accepts an array of numbers
representing the delay in milliseconds to wait before starting each reconnect attempt.

C#

HubConnection connection= new HubConnectionBuilder()


.WithUrl(new Uri("http://127.0.0.1:5000/chathub"))
.WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.FromSeconds(10) })
.Build();

// .WithAutomaticReconnect(new[] { TimeSpan.Zero,
TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)
}) yields the default behavior.

The preceding example configures the HubConnection to start attempting reconnects


immediately after the connection is lost. This is also true for the default configuration.

If the first reconnect attempt fails, the second reconnect attempt will also start
immediately instead of waiting 2 seconds like it would in the default configuration.

If the second reconnect attempt fails, the third reconnect attempt will start in 10
seconds which is again like the default configuration.
The custom behavior then diverges again from the default behavior by stopping after
the third reconnect attempt failure. In the default configuration there would be one
more reconnect attempt in another 30 seconds.

If you want even more control over the timing and number of automatic reconnect
attempts, WithAutomaticReconnect accepts an object implementing the IRetryPolicy
interface, which has a single method named NextRetryDelay .

NextRetryDelay takes a single argument with the type RetryContext . The RetryContext
has three properties: PreviousRetryCount , ElapsedTime and RetryReason , which are a
long , a TimeSpan and an Exception respectively. Before the first reconnect attempt,
both PreviousRetryCount and ElapsedTime will be zero, and the RetryReason will be the
Exception that caused the connection to be lost. After each failed retry attempt,
PreviousRetryCount will be incremented by one, ElapsedTime will be updated to reflect
the amount of time spent reconnecting so far, and the RetryReason will be the Exception
that caused the last reconnect attempt to fail.

NextRetryDelay must return either a TimeSpan representing the time to wait before the

next reconnect attempt or null if the HubConnection should stop reconnecting.

C#

public class RandomRetryPolicy : IRetryPolicy


{
private readonly Random _random = new Random();

public TimeSpan? NextRetryDelay(RetryContext retryContext)


{
// If we've been reconnecting for less than 60 seconds so far,
// wait between 0 and 10 seconds before the next reconnect attempt.
if (retryContext.ElapsedTime < TimeSpan.FromSeconds(60))
{
return TimeSpan.FromSeconds(_random.NextDouble() * 10);
}
else
{
// If we've been reconnecting for more than 60 seconds so far,
stop reconnecting.
return null;
}
}
}

C#

HubConnection connection = new HubConnectionBuilder()


.WithUrl(new Uri("http://127.0.0.1:5000/chathub"))
.WithAutomaticReconnect(new RandomRetryPolicy())
.Build();

Alternatively, you can write code that will reconnect your client manually as
demonstrated in Manually reconnect.

Manually reconnect
Use the Closed event to respond to a lost connection. For example, you might want to
automate reconnection.

The Closed event requires a delegate that returns a Task , which allows async code to
run without using async void . To satisfy the delegate signature in a Closed event
handler that runs synchronously, return Task.CompletedTask :

C#

connection.Closed += (error) => {


// Do your close logic.
return Task.CompletedTask;
};

The main reason for the async support is so you can restart the connection. Starting a
connection is an async action.

In a Closed handler that restarts the connection, consider waiting for some random
delay to prevent overloading the server, as shown in the following example:

C#

connection.Closed += async (error) =>


{
await Task.Delay(new Random().Next(0,5) * 1000);
await connection.StartAsync();
};

Call hub methods from client


InvokeAsync calls methods on the hub. Pass the hub method name and any arguments
defined in the hub method to InvokeAsync . SignalR is asynchronous, so use async and
await when making the calls.

C#
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);

The InvokeAsync method returns a Task which completes when the server method
returns. The return value, if any, is provided as the result of the Task . Any exceptions
thrown by the method on the server produce a faulted Task . Use await syntax to wait
for the server method to complete and try...catch syntax to handle errors.

The SendAsync method returns a Task which completes when the message has been
sent to the server. No return value is provided since this Task doesn't wait until the
server method completes. Any exceptions thrown on the client while sending the
message produce a faulted Task . Use await and try...catch syntax to handle send
errors.

7 Note

Calling hub methods from a client is only supported when using the Azure SignalR
Service in Default mode. For more information, see Frequently Asked Questions
(azure-signalr GitHub repository) .

Call client methods from hub


Define methods the hub calls using connection.On after building, but before starting the
connection.

C#

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


{
this.Dispatcher.Invoke(() =>
{
var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});

The preceding code in connection.On runs when server-side code calls it using the
SendAsync method.

C#
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user,message);
}

7 Note

While the hub side of the connection supports strongly-typed messaging, the client
must register using the generic method HubConnection.On with the method
name. For an example, see Host ASP.NET Core SignalR in background services.

Error handling and logging


Handle errors with a try-catch statement. Inspect the Exception object to determine the
proper action to take after an error occurs.

C#

try
{
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}

Additional resources
Hubs
JavaScript client
Publish to Azure
Azure SignalR Service serverless documentation
Microsoft.AspNetCore.SignalR.Client
Namespace
Reference

Contains types that are used for communicating with a SignalR server.

Classes
HubConnection A connection used to invoke hub methods on a SignalR Server.

HubConnectionBuilder A builder for configuring HubConnection instances.

HubConnectionBuilder Extension methods for IHubConnectionBuilder.


Extensions

HubConnectionBuilderHttp Extension methods for IHubConnectionBuilder.


Extensions

HubConnectionExtensions Extension methods for HubConnectionExtensions.

RetryContext The context passed to NextRetryDelay(RetryContext) to help the


policy determine how long to wait before the next retry and
whether there should be another retry at all.

Interfaces
IHubConnectionBuilder A builder abstraction for configuring HubConnection instances.

IRetryPolicy An abstraction that controls when the client attempts to


reconnect and how many times it does so.

Enums
HubConnectionState Describes the current state of the HubConnection to the server.

Remarks
For more information about the SignalR client, see ASP.NET Core SignalR .NET Client.
Feedback
Was this page helpful? ツ Yes ト No

Get help at Microsoft Q&A


ASP.NET Core SignalR Java client
Article • 01/14/2023

By Mikael Mengistu

The Java client enables connecting to an ASP.NET Core SignalR server from Java code,
including Android apps. Like the JavaScript client and the .NET client, the Java client
enables you to receive and send messages to a hub in real time. The Java client is
available in ASP.NET Core 2.2 and later.

The sample Java console app referenced in this article uses the SignalR Java client.

View or download sample code (how to download)

Install the SignalR Java client package


The signalr-7.0.0 JAR file allows clients to connect to SignalR hubs. To find the latest JAR
file version number, see the Maven search results .

If using Gradle, add the following line to the dependencies section of your build.gradle
file:

Gradle

implementation 'com.microsoft.signalr:signalr:7.0.0'

If using Maven, add the following lines inside the <dependencies> element of your
pom.xml file:

XML

<dependency>
<groupId>com.microsoft.signalr</groupId>
<artifactId>signalr</artifactId>
<version>1.0.0</version>
</dependency>

Connect to a hub
To establish a HubConnection , the HubConnectionBuilder should be used. The hub URL
and log level can be configured while building a connection. Configure any required
options by calling any of the HubConnectionBuilder methods before build . Start the
connection with start .

Java

HubConnection hubConnection = HubConnectionBuilder.create(input)


.build();

Call hub methods from client


A call to send invokes a hub method. Pass the hub method name and any arguments
defined in the hub method to send .

Java

hubConnection.send("Send", input);

7 Note

Calling hub methods from a client is only supported when using the Azure SignalR
Service in Default mode. For more information, see Frequently Asked Questions
(azure-signalr GitHub repository) .

Call client methods from hub


Use hubConnection.on to define methods on the client that the hub can call. Define the
methods after building but before starting the connection.

Java

hubConnection.on("Send", (message) -> {


System.out.println("New Message: " + message);
}, String.class);

Add logging
The SignalR Java client uses the SLF4J library for logging. It's a high-level logging API
that allows users of the library to choose their own specific logging implementation by
bringing in a specific logging dependency. The following code snippet shows how to
use java.util.logging with the SignalR Java client.

Gradle

implementation 'org.slf4j:slf4j-jdk14:1.7.25'

If you don't configure logging in your dependencies, SLF4J loads a default no-operation
logger with the following warning message:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".


SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further
details.

This can safely be ignored.

Android development notes


With regards to Android SDK compatibility for the SignalR client features, consider the
following items when specifying your target Android SDK version:

The SignalR Java Client will run on Android API Level 16 and later.
Connecting through the Azure SignalR Service will require Android API Level 20
and later because the Azure SignalR Service requires TLS 1.2 and doesn't support
SHA-1-based cipher suites. Android added support for SHA-256 (and above)
cipher suites in API Level 20.

Configure bearer token authentication


In the SignalR Java client, you can configure a bearer token to use for authentication by
providing an "access token factory" to the HttpHubConnectionBuilder. Use
withAccessTokenFactory to provide an RxJava Single<String> . With a call to
Single.defer , you can write logic to produce access tokens for your client.

Java

HubConnection hubConnection = HubConnectionBuilder.create("YOUR HUB URL


HERE")
.withAccessTokenProvider(Single.defer(() -> {
// Your logic here.
return Single.just("An Access Token");
})).build();

Passing Class information in Java


When calling the on , invoke , or stream methods of HubConnection in the Java client,
users should pass a Type object rather than a Class<?> object to describe any generic
Object passed to the method. A Type can be acquired using the provided

TypeReference class. For example, using a custom generic class named Foo<T> , the
following code gets the Type :

Java

Type fooType = new TypeReference<Foo<String>>() { }).getType();

For non-generics, such as primitives or other non-parameterized types like String , you
can simply use the built-in .class .

When calling one of these methods with one or more object types, use the generics
syntax when invoking the method. For example, when registering an on handler for a
method named func , which takes as arguments a String and a Foo<String> object, use
the following code to set an action to print the arguments:

Java

hubConnection.<String, Foo<String>>on("func", (param1, param2) ->{


System.out.println(param1);
System.out.println(param2);
}, String.class, fooType);

This convention is necessary because we can not retrieve complete information about
complex types with the Object.getClass method due to type erasure in Java. For
example, calling getClass on an ArrayList<String> would not return
Class<ArrayList<String>> , but rather Class<ArrayList> , which does not give the

deserializer enough information to correctly deserialize an incoming message. The same


is true for custom objects.

Known limitations
Transport fallback and the Server Sent Events transport aren't supported.
Additional resources
Java API reference
Use hubs in ASP.NET Core SignalR
ASP.NET Core SignalR JavaScript client
Publish an ASP.NET Core SignalR app to Azure App Service
Azure SignalR Service serverless documentation
com.microsoft.signalr
Reference
Package: com.microsoft.signalr
Maven Artifact: com.microsoft.signalr:signalr:5.0.10

This package contains the classes for SignalR Java client.

Classes
CancelInvocationMessage

CloseMessage

CompletionMessage

HttpHubConnectionBuilder A builder for configuring HubConnection instances.

HubConnection A connection used to invoke hub methods on a SignalR


Server.

HubConnectionBuilder A builder for configuring HubConnection instances.

HubException An exception thrown when the server fails to invoke a Hub


method.

HubMessage A base class for hub messages.

InvocationBindingFailureMessage

InvocationMessage

PingMessage

StreamBindingFailureMessage

StreamInvocationMessage

StreamItem

Subscription Represents the registration of a handler for a client method.

TypeReference<T> A utility for getting a Java Type from a literal generic Class.

UserAgentHelper

Interfaces
Action A callback that takes no parameters.

Action1<T1> A callback that takes one parameter.

Action2<T1,T2> A callback that takes two parameters.

Action3<T1,T2,T3> A callback that takes three parameters.

Action4<T1,T2,T3,T4> A callback that takes four parameters.

Action5<T1,T2,T3,T4,T5> A callback that takes five parameters.

Action6<T1,T2,T3,T4,T5,T6> A callback that takes six parameters.

Action7<T1,T2,T3,T4,T5,T6,T7> A callback that takes seven parameters.

Action8<T1,T2,T3,T4,T5,T6,T7,T8> A callback that takes eight parameters.

HubProtocol A protocol abstraction for communicating with SignalR hubs.

InvocationBinder An abstraction for passing around information about method


signatures.

OnClosedCallback A callback to create and register on a HubConnections On


Closed method.

Enums
HubConnectionState Indicates the state of the HubConnection.

HubMessageType

TransportEnum Used to specify the transport the client will use.

Feedback
Was this page helpful? ツ Yes ト No

Get help at Microsoft Q&A


ASP.NET Core SignalR JavaScript client
Article • 06/27/2022

By Rachel Appel

The ASP.NET Core SignalR JavaScript client library enables developers to call server-side
SignalR hub code.

Install the SignalR client package


The SignalR JavaScript client library is delivered as an npm package. The following
sections outline different ways to install the client library.

Install with npm

Visual Studio

Run the following commands from Package Manager Console:

Bash

npm init -y
npm install @microsoft/signalr

npm installs the package contents in the node_modules\@microsoft\signalr\dist\browser


folder. Create the wwwroot/lib/signalr folder. Copy the signalr.js file to the
wwwroot/lib/signalr folder.

Reference the SignalR JavaScript client in the <script> element. For example:

HTML

<script src="~/lib/signalr/signalr.js"></script>

Use a Content Delivery Network (CDN)


To use the client library without the npm prerequisite, reference a CDN-hosted copy of
the client library. For example:

HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-
signalr/6.0.1/signalr.js"></script>

The client library is available on the following CDNs:

cdnjs
jsDelivr
unpkg

Install with LibMan


LibMan can be used to install specific client library files from the CDN-hosted client
library. For example, only add the minified JavaScript file to the project. For details on
that approach, see Add the SignalR client library.

Connect to a hub
The following code creates and starts a connection. The hub's name is case insensitive:

JavaScript

const connection = new signalR.HubConnectionBuilder()


.withUrl("/chathub")
.configureLogging(signalR.LogLevel.Information)
.build();

async function start() {


try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.log(err);
setTimeout(start, 5000);
}
};

connection.onclose(async () => {
await start();
});

// Start the connection.


start();

Cross-origin connections (CORS)


Typically, browsers load connections from the same domain as the requested page.
However, there are occasions when a connection to another domain is required.

When making cross domain requests, the client code must use an absolute URL instead
of a relative URL. For cross domain requests, change .withUrl("/chathub") to
.withUrl("https://{App domain name}/chathub") .

To prevent a malicious site from reading sensitive data from another site, cross-origin
connections are disabled by default. To allow a cross-origin request, enable CORS:

C#

using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
builder.WithOrigins("https://example.com")
.AllowAnyHeader()
.WithMethods("GET", "POST")
.AllowCredentials();
});
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

// UseCors must be called before MapHub.


app.UseCors();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");
app.Run();

UseCors must be called before calling MapHub.

Call hub methods from the client


JavaScript clients call public methods on hubs via the invoke method of the
HubConnection. The invoke method accepts:

The name of the hub method.


Any arguments defined in the hub method.

In the following highlighted code, the method name on the hub is SendMessage . The
second and third arguments passed to invoke map to the hub method's user and
message arguments:

JavaScript

try

You might also like