Aspnet Core Aspnetcore 7.0
Aspnet Core Aspnetcore 7.0
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.
OVERVIEW DOWNLOAD
ASP.NET Core overview Download .NET
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
g
g Azure Storage
g Blob Storage
p Azure Table Storage
p Microsoft Graph scenarios
for 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
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.
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.
There are several advantages to targeting .NET Core, and these advantages increase
with each release. Some advantages of .NET Core over .NET Framework include:
1. Follow a tutorial for the app type you want to develop or maintain.
Scenario Tutorial
3. Read an overview of ASP.NET Core fundamentals that apply to all app types.
†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.
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#
To change the sample to run the ExpandDefault scenario, define the ExpandDefault
symbol and leave the remaining symbols commented-out:
C#
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 .
Next steps
For more information, see the following resources:
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 4.x
ASP.NET 4.x is a mature framework that provides the services needed to build
enterprise-grade, server-based web apps on Windows.
Framework selection
The following table compares ASP.NET Core to ASP.NET 4.x.
Razor Pages is the recommended approach to create a Web 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
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#
See ASP.NET Core targeting .NET Framework for information on ASP.NET Core 2.x
support on .NET Framework.
Additional resources
Introduction to ASP.NET
Introduction to ASP.NET Core
Deploy ASP.NET Core apps to Azure App Service
.NET vs. .NET Framework for server apps
Article • 10/04/2022
There are two supported .NET implementations for building server-side apps.
.NET .NET Core 1.0 - 3.1, .NET 5, and later versions of .NET.
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.
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.
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.
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 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.
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.
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.
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.
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.
At the end, you'll have a working web app running on your local machine.
Prerequisites
.NET 7.0 SDK
.NET CLI
Windows
.NET CLI
For more information, see Trust the ASP.NET Core HTTPS development certificate
.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.
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:
h WHAT'S NEW
May 2023
April 2023
March 2023
February 2023
January 2023
December 2022
e OVERVIEW
p CONCEPT
Contributor guide
Community
h WHAT'S NEW
Community
Community
h WHAT'S NEW
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:
SignalR
The following example shows the assignment of values that are double the default
values in ASP.NET Core 7.0 or earlier:
JavaScript
connection.serverTimeoutInMilliseconds = 60000;
connection.keepAliveIntervalInMilliseconds = 30000;
JavaScript
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;
};
}
});
JavaScript
Blazor.start({
configureSignalR: function (builder) {
builder.withServerTimeout(60000).withKeepAliveInterval(30000);
}
});
C#
builder.ServerTimeout = TimeSpan.FromSeconds(60);
builder.KeepAliveInterval = TimeSpan.FromSeconds(30);
await builder.StartAsync();
C#
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:
C#
using System.Text.Json.Serialization;
using MyFirstAotWebApi;
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0,
AppJsonSerializerContext.Default);
});
app.Run();
[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
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 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.
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.
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.
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#
...
// 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
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.
C#
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.
.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.
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:
C#
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseHttpSys();
var loggerFactory =
context.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Sample");
return next(context);
});
app.Run();
For more information, see Get detailed timing information with
IHttpSysRequestTimingFeature and Timing information and In-process hosting with IIS.
IAuthorizationRequirementData
Prior to ASP.NET Core 8, adding a parameterized authorization policy to an endpoint
required implementing an:
For example, consider the following sample written for ASP.NET Core 7.0:
C#
using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();
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;
}
requirement.Age);
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--;
}
return Task.CompletedTask;
}
}
The complete sample is here in the AspNetCore.Docs.Samples repository.
diff
using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
- builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();
app.MapControllers();
app.Run();
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;
}
Miscellaneous
The following sections describe miscellaneous new features in ASP.NET Core 8.
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);
}
[ProducesResponseType<T>]
[Produces<T>]
[MiddlewareFilter<T>]
[ModelBinder<T>]
[ModelMetadataType<T>]
[ServiceFilter<T>]
[TypeFilter<T>]
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:
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.
C#
@model Product?
C#
builder.Services.AddRazorPages();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.ConsentCookieValue = "true";
});
API controllers
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;
builder.Services.AddControllers();
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.DisableImplicitFromServicesParameters = true;
});
app.MapControllers();
app.Run();
The new mechanism to infer binding source of API Controller action parameters uses
the following rules:
C#
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
builder.Services.AddControllers(options =>
{
options.ModelMetadataDetailsProviders.Add(new
SystemTextJsonValidationMetadataProvider());
});
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
C#
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
builder.Services.AddControllers(options =>
{
options.ModelMetadataDetailsProviders.Add(new
NewtonsoftJsonValidationMetadataProvider());
}).AddNewtonsoftJson();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
For more information, see Use JSON property names in validation errors
Minimal APIs
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.
C#
// 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.
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
C#
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
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[]>));
}
}
[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);
Assert.NotNull(okResult.Value);
Assert.Equal(1, okResult.Value.Id);
}
IContentTypeHttpResult
IFileHttpResult
INestedHttpResult
IStatusCodeHttpResult
IValueHttpResult
IValueHttpResult<TValue>
C#
C#
C#
app.Run();
There is no built-in support for antiforgery. However, it can be implemented using the
IAntiforgery 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;
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#
return group;
}
In this scenario, you can use a relative address for the Location header in the 201
Created result:
C#
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#
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#
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.
.NET CLI
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:
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 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.
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
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.
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
For more information, see the following sections of the Routing and navigation article:
Navigation options
Handle/prevent location changes
) Important
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
@code {
private string searchText;
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.
Examples:
razor
@* Elements *@
@* Components *@
@code {
private string text = "";
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) .
For more information, see ASP.NET Core Blazor forms and input components.
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.
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.
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
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.
WebAssembly Single Instruction, Multiple Data (SIMD) support (only with AOT,
not supported by Apple Safari)
WebAssembly exception handling support
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:
Performance
For more information, see Overview of caching and Output caching middleware.
HTTP/3 improvements
This release:
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;
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:
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.
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.
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.
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
IIS
dotnet watch
.NET CLI
With Visual Studio, select the new Do not use top-level statements checkbox during
project creation:
C#
services.AddW3CLogging(logging =>
{
logging.AdditionalRequestHeaders.Add("x-forwarded-for");
logging.AdditionalRequestHeaders.Add("x-client-ssl-protocol");
});
Request decompression
The new Request decompression middleware:
This article highlights the most significant changes in ASP.NET Core 6.0 with links to
relevant documentation.
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:
SignalR
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.
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.
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)
.NET CLI
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.
When implementing this interface, use the DisposeAsync method to release resources.
C#
C#
_jsonWriter = null;
}
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
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
Blazor Server
Blazor WebAssembly
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.
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.
Required parameters
Apply the new [EditorRequired] attribute to specify a required component parameter.
For more information, see ASP.NET Core Razor components.
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).
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.
) Important
Blazor Hybrid is in preview and shouldn't be used in production apps until final
release.
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 .
HeartbeatSlow .
Microsoft.AspNetCore.Server.Kestrel.BadRequests : ConnectionBadRequest ,
RequestProcessingError , RequestBodyMinimumDataRateNotSatisfied .
Microsoft.AspNetCore.Server.Kestrel.Connections : ConnectionAccepted ,
ConnectionStart , ConnectionStop , ConnectionPause , ConnectionResume ,
ApplicationAbortedConnection .
Microsoft.AspNetCore.Server.Kestrel.Http2 : Http2ConnectionError ,
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
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;
app.Run();
See this example of a custom IConnectionListenerFactory which shows how to use this
SocketConnectionContextFactory .
IIS Express is still available as a launch profile for scenarios such as Windows
Authentication or port sharing.
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.
C#
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.WebUtilities;
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 .
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.
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>
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
diff
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
+ "Microsoft.AspNetCore": "Warning"
C#
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
C#
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
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 />
C#
builder.Services.AddRazorPages()
.AddNewtonsoftJson(options =>
{
options.OutputFormatterMemoryBufferThreshold = 48 * 1024;
});
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#
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.
C#
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#
See the related announcement for additional details about this change in behavior.
C#
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
JSON
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware":
"Information"
}
}
}
C#
using Microsoft.AspNetCore.HttpLogging;
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#
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 .
C#
using StackExchange.Redis.Profiling;
builder.Services.AddStackExchangeRedisCache(options =>
{
options.ProfilingSession = () => new ProfilingSession();
});
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
<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>
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.
For example, the following PersonController uses the Person record type with model
binding and form validation:
C#
[HttpPost]
public IActionResult Index(Person person)
{
// ...
}
}
CSHTML
@model Person
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
In ASP.NET Core 5.0, the web API templates enable the OpenAPI support by default. To
disable OpenAPI:
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>
C#
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:
The template generated code won't accidentally expose the API's description when
publishing to production.
C#
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
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.
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.
Blazor events now support the ontoggle DOM event. For more information, see ASP.NET
Core Blazor event handling.
IAsyncDisposable support
Razor components now support the IAsyncDisposable interface for the asynchronous
release of allocated resources.
InputDate
InputNumber
InputSelect
For more information, see ASP.NET Core Blazor forms and input components.
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.
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.
gRPC
Many preformance improvements have been made in gRPC . For more information,
see gRPC performance improvements in .NET 5 .
For more information, see Use hub filters in ASP.NET Core SignalR.
C#
Java
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.
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#
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.
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#
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
C#
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
})
.AllowAnonymous();
});
}
API improvements
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#
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#
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.
C#
await host.RunAsync();
}
}
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.
C#
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.
CSHTML
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.
CSHTML
C#
razor
razor
<div @onclick="OnSelectParentDiv">
<div @onclick="OnSelectChildDiv"
@onclick:stopPropagation="_stopPropagation">
...
</div>
</div>
@code {
private bool _stopPropagation = false;
}
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:
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.
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 :
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
JavaScript
A custom implementation can be passed in for full control of the reconnection intervals.
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:
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;
});
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
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#
if (IsUserAllowedToDoThis(resource.HubMethodName,
context.User.Identity.Name))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
return currentUsername.EndsWith("@jabbr.net",
StringComparison.OrdinalIgnoreCase));
}
}
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:
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)
{
}
}
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");
});
C#
After the for loop has completed and the local function exits, the stream completion is
sent:
C#
JavaScript client apps use the SignalR Subject (or an RxJS Subject ) for the stream
argument of the UploadStream Hub method above.
JavaScript
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.
To add Json.NET to ASP.NET Core 3.0, see Add Newtonsoft.Json-based JSON format
support.
@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 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core 3.0. It
enables the following security features:
C#
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#
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.
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 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_
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.
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:
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");
});
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.
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:
For migration information, see Port your code from .NET Framework to .NET Core.
XML
<Project Sdk="Microsoft.NET.Sdk.Web">
This article highlights the most significant changes in ASP.NET Core 2.2, with links to
relevant documentation.
Endpoint Routing
ASP.NET Core 2.2 uses a new endpoint routing system for improved dispatching of
requests. The changes include new link generation API members and route parameter
transformers.
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.
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:
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.
Kestrel configuration
In earlier versions of ASP.NET Core, Kestrel options are configured by calling UseKestrel .
In 2.2, Kestrel options are configured by calling ConfigureKestrel on the host builder.
This change resolves an issue with the order of IServer registrations for in-process
hosting. For more information, see the following resources:
CORS improvements
In earlier versions of ASP.NET Core, CORS Middleware allows Accept , Accept-Language ,
Content-Language , and Origin headers to be sent regardless of the values configured in
CorsPolicy.Headers . In 2.2, a CORS Middleware policy match is only possible when the
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.
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.
For more information, see Create reusable UI using the Razor Class Library project.
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.
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.
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.
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 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());
}
}
[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:
The Angular template is based on the Angular CLI, and the React template is based on
create-react-app.
For more information, see Compatibility version for ASP.NET Core MVC.
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 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.
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.
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:
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:
For more information, see Kestrel web server implementation in ASP.NET Core.
For more information, see HTTP.sys web server implementation in ASP.NET Core.
The file returned to your visitors 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.
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 .
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.
XML
<LangVersion>latest</LangVersion>
For information about the status of C# 7.1 features, see the Roslyn GitHub repository .
Migration guidance
For guidance on how to migrate ASP.NET Core 1.x applications to ASP.NET Core 2.0, see
the following resources:
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
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
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.
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.
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.
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.
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:
Using .NET for client-side web development offers the following advantages:
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.
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.
Benefits of ASP.NET Core SPA with JavaScript Frameworks, in addition to the client
rendering benefits previously listed:
Downsides:
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:
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.
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
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.
Select Create.
The following starter project is created:
For alternative approaches to create the project, see Create a new project in Visual
Studio.
Visual Studio displays the following dialog when a project is not yet configured to
use SSL:
Visual Studio:
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#
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
C#
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#
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.
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
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.
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.
C#
using System.ComponentModel.DataAnnotations;
namespace RazorPagesMovie.Models;
The question mark after string indicates that the property is nullable. For
more information, see Nullable reference types.
Visual Studio
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.
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);
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Visual Studio
In this section, the Package Manager Console (PMC) window is used to:
1. From the Tools menu, select NuGet Package Manager > Package Manager
Console.
PowerShell
Add-Migration InitialCreate
Update-Database
No type was specified for the decimal column 'Price' on entity type 'Movie'. This will
cause values to be silently truncated if they do not fit in the default precision and
scale. Explicitly specify the SQL server column type that can accommodate all the
values using 'HasColumnType()'.
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)
{
}
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 ).
Console
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 .
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);
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
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.
C#
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies;
Razor Pages are derived from PageModel. By convention, the PageModel derived class is
named PageNameModel . For example, the Index page is named IndexModel .
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;
C#
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
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#.
@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.
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.
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.
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.
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
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>
CSHTML
<a class="navbar-brand" asp-area="" asp-
page="/Index">RazorPagesMovie</a>
CSHTML
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.
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.
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;
}
[BindProperty]
public Movie Movie { get; set; } = default!;
_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#
_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.
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">
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
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);
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
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
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
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.
C#
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
namespace RazorPagesMovie.Models;
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;
}
Visual Studio
C#
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
SeedData.Initialize(services);
}
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
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'.
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.
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models;
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
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,
CSHTML
@page "{id:int?}"
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#
if (Movie == null)
{
return NotFound();
}
return Page();
}
C#
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
The previous code detects concurrency exceptions when one client deletes the movie
and the other client posts changes to the movie.
To test the catch block:
Production code may want to detect concurrency conflicts. See Handle concurrency
conflicts for more information.
C#
[BindProperty]
public Movie Movie { get; set; } = default!;
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
When an HTTP GET request is made to the Movies/Edit page, for example,
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
on the page.
The Edit form is displayed with the values from the movie.
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.
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
C#
[BindProperty(SupportsGet = true)]
public string? SearchString { get; set; }
[BindProperty(SupportsGet = true)]
public string? MovieGenre { get; set; }
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
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.
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#
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));
}
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
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.*@
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
Search by genre
Update the Index page's OnGetAsync method with the following code:
C#
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#
C#
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>
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
When using EF Code First to automatically create and track a database, Code First:
Automatic verification that the schema and model are in sync makes it easier to find
inconsistent database code issues.
C#
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>
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 :
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.
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.
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"
},
Visual Studio
PowerShell
Add-Migration Rating
Update-Database
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:
4. Select OK.
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:
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.
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;
[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.
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 .
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.
2. Set a break point in the OnPostAsync method of the Create or Edit page.
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:
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.
C#
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
The [DataType] attributes provide:
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.
Can enable the app to automatically provide type-specific features. For example, a
mailto: link can be created for DataType.EmailAddress .
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 .
C#
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#
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.
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models;
[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#
SQL
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
C#
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)");
}
SQL
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:
Prerequisites
Visual Studio
Visual Studio 2022 with the ASP.NET and web development workload.
Create a web app
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.
Visual Studio
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.
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:
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:
In the next tutorial in this series, you learn about MVC and start writing some code.
By Rick Anderson
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:
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 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.
C#
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers;
An HTTP endpoint:
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
Append /HelloWorld to the path in the address bar. The Index method returns a string.
MVC invokes controller classes, and the action methods within them, depending on the
incoming URL. The default URL routing logic used by MVC, uses a format like this to
determine what code to invoke:
/[Controller]/[ActionName]/[Parameters]
C#
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 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
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}");
}
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.
C#
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
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.
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#
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.
Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the
following:
CSHTML
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
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:
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.
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>
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
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.
@{
Layout = "_Layout";
}
CSHTML
@{
ViewData["Title"] = "Movie List";
}
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
CSHTML
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 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.
Controllers are responsible for providing the data required in order for a view template
to render a response.
Do business logic
Interact with a database directly.
A view template should work only with the data that's provided to it by the controller.
Maintaining this "separation of concerns" helps keep the code:
Clean.
Testable.
Maintainable.
Currently, the Welcome method in the HelloWorldController class takes a name and 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.
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;
The ViewData dictionary object contains data that will be passed to the view.
CSHTML
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4
Data is taken from the URL and passed to the controller using the MVC model binder.
The controller packages the data into a ViewData dictionary and passes that object to
the view. The view then renders the data as HTML to the browser.
In 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 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.
Right-click the Models folder > Add > Class. Name the file Movie.cs .
C#
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models;
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.
The question mark after string indicates that the property is nullable. For more
information, see Nullable reference types.
Visual Studio
In Solution Explorer, right-click the Controllers folder and select Add > New
Scaffolded Item.
In the Add New Scaffolded Item dialog:
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.VisualStudio.Web.CodeGeneration.Design
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 .
PowerShell
Add-Migration InitialCreate
Update-Database
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.
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()'.
If you get an exception similar to the following, you may have missed the Update-
Database command in the migrations step:
Console
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 .
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)
{
}
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.
Visual Studio
C#
builder.Services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));
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.
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);
});
}
InitialCreate.Up creates the Movie table and configures Id as the primary key.
C#
MVC provides the ability to pass strongly typed model objects to a view. This strongly
typed approach enables compile time code checking. The scaffolding mechanism
passed a strongly typed model in the MoviesController class and views.
C#
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
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.
C#
If a movie is found, an instance of the Movie model is passed to the Details view:
C#
return View(movie);
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
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#
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
C#
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;
namespace MvcMovie.Models;
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.
}
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;
builder.Services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));
SeedData.Initialize(services);
}
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.
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;
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.
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
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>
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Tag Helpers are one of the most popular new features in ASP.NET Core. For more
information, see Additional resources.
Open the Movies controller and examine the two Edit action methods. The following
code shows the HTTP GET Edit method, which fetches the movie and populates the edit
form generated by the Edit.cshtml Razor file.
C#
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted
movie values:
C#
// 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.
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();
}
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
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
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 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.
C#
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
The following line in the Index action method creates a LINQ query to select the
movies:
C#
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));
}
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
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?}");
C#
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
C#
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#
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>
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.
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>
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.
CSHTML
<form asp-controller="Movies" asp-action="Index" method="get">
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models;
A list of movies.
A SelectList containing the list of genres. This allows the user to select a genre
from the list.
MovieGenre , which contains the selected genre.
SearchString , which contains the text users enter in the search text box.
C#
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string
searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
return View(movieGenreVM);
}
The following code is a LINQ query that retrieves all the genres from the database.
C#
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.
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>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies!)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-
id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-
id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
In the preceding code, the DisplayNameFor HTML Helper inspects the Title property
referenced in the lambda expression to determine the display name. Since the lambda
expression is inspected rather than evaluated, you don't receive an access violation
when model , model.Movies , or model.Movies[0] are null or empty. When the lambda
expression is evaluated (for example, @Html.DisplayFor(modelItem => item.Title) ), the
model's property values are evaluated. The ! after model.Movies is the null-forgiving
operator, which is used to declare that Movies isn't null.
Previous Next
Part 8, add a new field to an ASP.NET
Core MVC app
Article • 05/02/2023
By Rick Anderson
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models;
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.
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>
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:
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.)
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.
From the Tools menu, select NuGet Package Manager > Package Manager
Console.
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
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:
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.
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;
[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 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.
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>
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.
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#
[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 .
C#
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:
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.
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models;
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
C#
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The MVC scaffolding engine that created this action method adds a comment showing
an HTTP request that invokes the method. In this case it's a GET request with three URL
segments, the Movies controller, the Details method, and an id value. Recall these
segments are defined in Program.cs .
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
movie). If you didn't check for a null movie, the app would throw an exception.
C#
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
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.
Use ASP.NET Core SignalR with Blazor (Blazor Server or Blazor WebAssembly)
Learn modules
Tutorial: Create a web API with ASP.NET
Core
Article • 05/17/2023
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:
GET /api/todoitems Get all to-do items None Array of to-do items
Visual Studio 2022 with the ASP.NET and web development workload.
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 .
Visual Studio
Visual Studio displays the following dialog when a project is not yet configured to
use SSL:
The Swagger page /swagger/index.html is displayed. Select GET > Try it out > Execute.
The page displays:
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.
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"
}
]
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;
Model classes can go anywhere in the project, but the Models folder is used by
convention.
Visual Studio
C#
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models;
C#
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Scaffold a controller
Visual Studio
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.
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.
C#
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute.
The method gets the value of the TodoItem from the body of the HTTP request.
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.
In the Swagger browser window, select GET /api/TodoItems/{id}, and then select
Try it out.
GET /api/todoitems
GET /api/todoitems/{id}
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.
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.
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
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.
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.
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 .
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();
}
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.
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.
C#
namespace TodoApi.Models;
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;
// 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();
}
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();
}
Duende Identity Server is an OpenID Connect and OAuth 2.0 framework for ASP.NET
Core. Duende Identity Server enables the following security features:
) 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.
This tutorial creates a web API that runs Create, Read, Update, and Delete (CRUD)
operations on a MongoDB NoSQL database.
" 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:
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
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 .
2. In the command shell connect to the default test database by running the
following command:
Console
mongosh
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.
Console
db.createCollection('Books')
Console
{ "ok" : 1 }
5. Define a schema for the Books collection and insert two documents using the
following command:
Console
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.
db.Books.find().pretty()
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.
2. Select the ASP.NET Core Web API project type, 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
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;
[BsonElement("Name")]
public string BookName { get; set; } = null!;
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 .
JSON
{
"BookStoreDatabase": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BookStore",
"BooksCollectionName": "Books"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
C#
namespace BookStoreApi.Models;
C#
C#
using BookStoreApi.Models;
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 BooksService(
IOptions<BookStoreDatabaseSettings> bookStoreDatabaseSettings)
{
var mongoClient = new MongoClient(
bookStoreDatabaseSettings.Value.ConnectionString);
_booksCollection = mongoDatabase.GetCollection<Book>(
bookStoreDatabaseSettings.Value.BooksCollectionName);
}
C#
builder.Services.AddSingleton<BooksService>();
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);
_booksCollection = mongoDatabase.GetCollection<Book>(
bookStoreDatabaseSettings.Value.BooksCollectionName);
}
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;
[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);
[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;
[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();
}
}
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"
}
]
JSON
{
"id": "61a6058e6c43f32854e51f52",
"bookName": "Clean Code",
"price": 43.15,
"category": "Computers",
"author": "Robert C. Martin"
}
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 .
C#
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 .
C#
[BsonElement("Name")]
[JsonPropertyName("Name")]
public string BookName { get; set; } = null!;
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.
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
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;
builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
if (builder.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
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">✖</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>
5. Add a CSS file named site.css to the wwwroot/css folder. Replace the contents of
site.css with the following styles:
css
#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
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);
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 tr = tBody.insertRow();
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.
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.
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.
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));
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:
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.
The main view of the items, as shown above, lists each item's name and indicates if it's
done with a checkmark.
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}";
You can optionally deploy the web service to a cloud service such as Azure and update
the RestUrl .
C#
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; }
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();
}
_todoList.Add(todoItem1);
_todoList.Add(todoItem2);
_todoList.Add(todoItem3);
}
}
}
C#
C#
[ApiController]
[Route("api/[controller]")]
public class TodoItemsController : ControllerBase
{
private readonly ITodoRepository _todoRepository;
This API supports four different HTTP verbs to perform CRUD (Create, Read, Update,
Delete) operations on the data source. The simplest of these is the Read operation,
which corresponds to an HTTP GET request.
Reading Items
Requesting a list of items is done with a GET request to the List method. The
[HttpGet] attribute on the List method indicates that this action should only handle
GET requests. The route for this action is the route specified on the controller. You don't
necessarily need to use the action name as part of the route. You just need to ensure
each action has a unique and unambiguous route. Routing attributes can be applied at
both the controller and method levels to build up specific routes.
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#
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.
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.
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.
C#
...
builder.Services.AddSwaggerGen();
...
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
...
C#
...
app.UseSwagger();
if (app.Environment.IsDevelopment())
{
app.UseSwaggerUI();
}
...
C#
[ApiController]
[Route("/")]
public class WeatherForecastController : ControllerBase
2. In the Publish dialog, select Azure and select the Next button.
3. Select Azure App Service (Windows) and select the Next button.
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.
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.
8. Select the Create button to proceed with the API Management service creation.
This step may take several minutes to complete.
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.
13. Select the API you created in the preceding steps. It's now populated and you can
explore around.
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.
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.
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
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:
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
Prerequisites
Visual Studio
Visual Studio 2022 with the ASP.NET and web development workload.
Create an API project
Visual Studio
C#
app.Run();
Visual Studio
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.
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.
C#
The preceding code creates the model for this app. A model is a class that represents
data that the app manages.
C#
using Microsoft.EntityFrameworkCore;
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.
C#
using Microsoft.EntityFrameworkCore;
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
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#
The DI container provides access to the database context and other services.
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
C#
Run the app. The browser displays a 404 error because there is no longer a / endpoint.
Select raw.
JSON
{
"name":"walk dog",
"isComplete":true
}
Select Send.
Examine the GET endpoints
The sample app implements several GET endpoints by calling MapGet :
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
C#
Select Send.
JSON
[
{
"id": 1,
"name": "walk dog",
"isComplete": false
}
]
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.
C#
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.
Update the to-do item that has Id = 1 and set its name to "feed fish" :
JSON
{
"id": 1,
"name": "feed fish",
"isComplete": false
}
C#
return Results.NotFound();
});
C#
using Microsoft.EntityFrameworkCore;
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
return Results.NotFound();
});
app.Run();
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;
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
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#
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
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#
Unit test code can verify that an object of type Ok<Todo[]> is returned from the handler
method. For example:
C#
// Act
var result = await TodosApi.GetAllTodos(db);
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.
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#
The secret field needs to be hidden from this app, but an administrative app could
choose to expose it.
C#
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name,
todoItem.IsComplete);
}
C#
using Microsoft.EntityFrameworkCore;
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.Ok(todoItemDTO);
}
return TypedResults.NotFound();
}
Verify you can post and get all fields except the secret field.
Next steps
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:
Prerequisites
Visual Studio
Visual Studio 2022 with the ASP.NET and web development workload.
Create a web app project
Visual Studio
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.
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 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;
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.
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>
In the wwwroot/js folder, create a chat.js file with the following code:
JavaScript
"use strict";
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();
});
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.
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.
.NET CLI
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.
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:
.
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.
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.
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
C#
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.
C#
using Microsoft.AspNetCore.SignalR;
namespace SignalRWebpack.Hubs;
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:
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");
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
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"
},
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
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 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:
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
*,
*::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;
}
JSON
{
"compilerOptions": {
"target": "es5"
}
}
index.ts :
TypeScript
divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});
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
is called.
Console
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.
Visual Studio
Console
This command generates the client-side assets to be served when running the
app. The assets are placed in the wwwroot folder.
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.
Prerequisites
Visual Studio
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.
Visual Studio
7 Note
Visual Studio 2022 or later and .NET Core SDK 6.0.0 or later are required.
Type BlazorServerSignalRApp in the Project name field. Confirm the Location entry
is correct or provide a location for the project. Select Next.
Select Create.
In the Manage NuGet Packages dialog, confirm that the Package source is set to
nuget.org .
If the License Acceptance dialog appears, select I Accept if you agree with the
license terms.
C#
using Microsoft.AspNetCore.SignalR;
namespace BlazorServerSignalRApp.Server.Hubs;
C#
using Microsoft.AspNetCore.ResponseCompression;
using BlazorServerSignalRApp.Server.Hubs;
C#
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
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");
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;
await hubConnection.StartAsync();
}
7 Note
Visual Studio
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:
Next steps
In this tutorial, you learned how to:
To learn more about building Blazor apps, see the Blazor documentation:
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.
Prerequisites
Visual Studio
Visual Studio 2022 with the ASP.NET and web development workload.
Visual Studio
Visual Studio displays the following dialog when a project is not yet
configured to use SSL:
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
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.
Protos/greet.proto : defines the Greeter gRPC and is used to generate the gRPC
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.
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.
Visual Studio
Install the packages using either the Package Manager Console (PMC) or Manage
NuGet Packages.
From Visual Studio, select Tools > NuGet Package Manager > Package
Manager Console
PowerShell
Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package 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
Visual Studio
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>
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 :
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.
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();
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
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
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
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
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 .
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 .
Visual Studio
PowerShell
Update-Database
Run the project to seed the database.
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.
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>
<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>
The layout file sets the site header, footer, and menu. The preceding code makes the
following changes:
In Pages/Index.cshtml , replace the contents of the file with the following code:
CSHTML
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
A student can enroll in any number of courses, and a course can have any number of
students enrolled in it.
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; }
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.
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.
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
The EnrollmentID property is the primary key; this entity uses the classnameID pattern
instead of ID by itself. For a production data model, 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
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; }
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.
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
ContosoUniversity.Data.SchoolContext
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.VisualStudio.Web.CodeGeneration.Design
If the preceding step fails, build the project and retry the scaffold step.
Creates Data/SchoolContext.cs .
Adds the context to dependency injection in Program.cs .
Adds a database connection string to appsettings.json .
Visual Studio
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 .
C#
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Models;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext (DbContextOptions<SchoolContext> options)
: base(options)
{
}
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.
Because an entity set contains multiple entities, many developers prefer the DBSet
property names should be plural.
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
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();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
else
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
In the Package Manager Console, enter the following to add the NuGet package:
PowerShell
Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
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();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
else
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
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:
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.
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
}
context.Students.AddRange(students);
context.SaveChanges();
context.Courses.AddRange(courses);
context.SaveChanges();
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.
C#
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.
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.
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#
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
C#
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.
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
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.
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#
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#
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
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.
C#
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#
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.
C#
The following code uses the StudentVM view model to create a new student:
C#
[BindProperty]
public StudentVM StudentVM { get; set; }
The SetValues method sets the values of this object by reading values from another
PropertyValues object. SetValues uses property name matching. The view model type:
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");}
}
C#
if (Student == null)
{
return NotFound();
}
return Page();
}
if (studentToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
The code changes are similar to the Create page with a few exceptions:
FirstOrDefaultAsync has been replaced with FindAsync. When you don't have to
The current student is fetched from the database, rather than creating an empty
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
Deleted : The entity has been marked for deletion. The SaveChanges method issues
a DELETE statement.
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.
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;
[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }
if (Student == null)
{
return NotFound();
}
if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = String.Format("Delete {ID} failed. Try
again", id);
}
return Page();
}
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 });
}
}
}
}
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
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:
CSHTML
@page
@model ContosoUniversity.Pages.Students.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@Model.ErrorMessage</p>
<form method="post">
<input type="hidden" asp-for="Student.ID" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
Run the app and delete a student to test the Delete page.
Next steps
Previous tutorial Next tutorial
Part 3, Razor Pages with EF Core in
ASP.NET Core - Sort, Filter, Paging
Article • 04/11/2023
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#
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s =>
s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
The OnGetAsync method receives a sortOrder parameter from the query string in the
URL. The URL 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#
The code uses the C# conditional operator ?:. The ?: operator is a ternary operator, it
takes three operands. The first line specifies that when sortOrder is null or empty,
NameSort is set to name_desc . If sortOrder is not null or empty, NameSort is set to an
empty string.
These two statements enable the page to set the column heading hyperlinks as follows:
The method uses LINQ to Entities to specify the column to sort by. The code initializes
an IQueryable<Student> before the switch statement, and modifies it in the switch
statement:
C#
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
When an IQueryable is created or modified, no query is sent to the database. The query
isn't executed until the IQueryable object is converted into a collection. IQueryable are
converted to a collection by calling a method such as ToListAsync . Therefore, the
IQueryable code results in a single query that's not executed until the following
statement:
C#
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.
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>
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.
C#
CurrentFilter = searchString;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s =>
s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
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.
be different.
C#
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.
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 .
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 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.
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.
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);
}
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.
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"
}
}
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;
CurrentFilter = searchString;
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 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>
<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>
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>
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.
C#
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
CSHTML
@page
@model ContosoUniversity.Pages.AboutModel
@{
ViewData["Title"] = "Student Body Statistics";
}
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
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;
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.
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:
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:
Use SQL Server Object Explorer (SSOX) to delete the database, or run the following
command in the Package Manager Console (PMC):
PowerShell
Drop-Database
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.
C#
context.Database.EnsureCreated();
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");
}
migrationBuilder.DropTable(
name: "Course");
migrationBuilder.DropTable(
name: "Student");
}
}
}
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."
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.
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.
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
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.
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:
In the preceding Dataedo diagram, the CourseInstructor is a join table created by Entity
Framework. For more information, see Many-to-many
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;
}
}
The preceding code adds a FullName property and adds the following attributes to
existing properties:
[DataType]
[DisplayFormat]
[StringLength]
[Column]
[Required]
[Display]
two other properties. FullName can't be set, so it has only a get accessor. No FullName
column is created in the database.
[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 DataType attribute emits HTML 5 data- (pronounced data dash) attributes. The
DataType attributes don't provide validation.
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
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.
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.
[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
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#
MinimumLength and Required allow whitespace to satisfy the validation. Use the
RegularExpression attribute for full control over the string.
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:
SchoolContext
In the PMC, enter the following commands to create a new migration and
update the database:
PowerShell
Add-Migration ColumnFirstName
Update-Database
text
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.
7 Note
In the following sections, building the app at some stages generates compiler
errors. The instructions specify when to build the app.
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; }
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.
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#
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; }
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.
When an Instructor entity has a related OfficeAssignment entity, each entity has a
reference to the other one in its navigation property.
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; }
[Range(0, 5)]
public int Credits { get; set; }
The Course entity has a foreign key (FK) property DepartmentID . DepartmentID points to
the related Department entity. The Course entity has a Department navigation property.
EF Core doesn't require a foreign key property for a data model when the model has a
navigation property for a related entity. EF Core automatically creates FKs in the
database wherever they're needed. EF Core creates shadow properties for automatically
created FKs. However, explicitly including the FK in the data model can make updates
simpler and more efficient. For example, consider a model where the FK property
DepartmentID is not included. When a course entity is fetched to edit:
When the FK property DepartmentID is included in the data model, there's no need to
fetch the Department entity before an update.
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.
C#
A course can have any number of students enrolled in it, so the Enrollments navigation
property is a collection:
C#
C#
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; }
[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; }
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:
C#
C#
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.
C#
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
C#
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
An enrollment record is for one course, so there's a CourseID FK property and a Course
navigation property:
C#
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.
C#
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}
The preceding code adds the new entities and configures the many-to-many
relationship between the Instructor and Course entities.
C#
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:
For more information about attributes vs. fluent API, see Methods of configuration.
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
}
context.AddRange(students);
context.AddRange(instructors);
context.AddRange(officeAssignments);
context.AddRange(departments);
context.AddRange(courses);
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.
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.
Visual Studio
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
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.
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.
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.
Run the app and select the Courses link. The department column displays the
DepartmentID , which isn't useful.
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;
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.
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>
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
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#
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; }
}
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.
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; }
}
}
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.
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;
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.
C#
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
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;
}
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>
</table>
}
https://localhost:5001/Instructors?id=2
HTML
Adds a Courses column that displays courses taught by each instructor. See Explicit
line transition for more about this razor syntax.
HTML
Adds a new hyperlink labeled Select. This link sends the selected instructor's ID to
the Index method and sets a background color.
HTML
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.
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.
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; }
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 .
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;
[BindProperty]
public Course Course { get; set; }
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");
}
If you would like to see code comments translated to languages other than English, let
us know in this GitHub discussion issue .
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");}
}
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;
[BindProperty]
public Course Course { get; set; }
if (Course == null)
{
return NotFound();
}
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");
}
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
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");}
}
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.
C#
if (Course == null)
{
return NotFound();
}
return Page();
}
CSHTML
@page
@model ContosoUniversity.Pages.Courses.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<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>
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>
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.
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;
or not the instructor is assigned to the course. A HashSet is used for efficient lookups.
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.
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;
[BindProperty]
public Instructor Instructor { get; set; }
if (Instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(_context, Instructor);
return Page();
}
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();
}
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.
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);
}
}
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;
<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.
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;
[BindProperty]
public Instructor Instructor { get; set; }
if (selectedCourses.Length > 0)
{
newInstructor.Courses = new List<Course>();
// Load collection with one DB call.
_context.Courses.Load();
}
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();
}
}
}
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#
if (selectedCourses.Length > 0)
{
newInstructor.Courses = new List<Course>();
// Load collection with one DB call.
_context.Courses.Load();
}
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();
}
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;
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
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;
[BindProperty]
public Instructor Instructor { get; set; }
if (instructor == null)
{
return RedirectToPage("./Index");
}
_context.Instructors.Remove(instructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
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.
Next steps
Previous tutorial Next tutorial
Part 8, Razor Pages with EF Core in
ASP.NET Core - Concurrency
Article • 04/11/2023
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:
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.
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.
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.
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.
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; }
[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; }
[Timestamp]
public byte[] ConcurrencyToken { get; set; }
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");
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
The following highlighted code shows the T-SQL that verifies exactly one row was
updated:
SQL
Add a migration
Adding the ConcurrencyToken property changes the data model, which requires a
migration.
Visual Studio
PowerShell
Add-Migration RowVersion
Update-Database
The preceding commands:
C#
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Follow the instructions in Scaffold Student pages with the following exceptions:
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:
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>
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;
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
if (Department == null)
{
return NotFound();
}
return Page();
}
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
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();
}
return Page();
}
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.");
}
}
}
C#
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
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
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:
C#
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
if (Department == null)
{
return NotFound();
}
return Page();
}
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
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();
}
C#
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();
}
precedence over the model property values when both are present.
diff
+ 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");}
}
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
Click Save again. The value you entered in the second browser tab is saved. You see the
saved values in the Index page.
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;
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
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();
}
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:
CSHTML
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<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>
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
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:
Some things the Razor Pages tutorial has that this one doesn't:
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
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:
Some things the Razor Pages tutorial has that this one doesn't:
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.
Tip
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
<!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>
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 »</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 »</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.
PowerShell
Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
For information about other database providers that are available for EF Core, see
Database providers.
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; }
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 .
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.
In the Models folder, create the Enrollment class with the following code:
C#
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
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.
CourseID .
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; }
which entities are included in the data model. Some EF behaviors can be customized. In
this project, the class is named SchoolContext .
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)
{
}
The preceding code creates a DbSet property for each entity set. In EF terminology:
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)
{
}
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;
}
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": "*"
}
C#
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
);
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddControllersWithViews();
}
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();
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();
}
C#
host.Run();
}
The first time the app is run, the database is created and loaded with test data.
Whenever the data model changes:
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:
The Visual Studio scaffolding engine creates a StudentsController.cs file and a set of
views ( *.cshtml files) that work with the controller.
C#
namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;
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#
The asynchronous programming elements in this code are explained later in the tutorial.
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:
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#
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,
For more information about asynchronous programming in .NET, see Async Overview.
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.
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
Prerequisites
Get started with EF Core and ASP.NET Core MVC
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#
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
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
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
HTML
<a href="/Students/Edit?studentID=6">Edit</a>
For more information about tag helpers, see Tag Helpers in ASP.NET Core.
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:
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).
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.
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.
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 .
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
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
Deleted . The entity has been marked for deletion. The SaveChanges method issues
a DELETE statement.
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 .
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#
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.
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.
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.
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.)
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.
Advance to the next tutorial to learn how to expand the functionality of the Index page
by adding sorting, filtering, and paging.
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.
Prerequisites
Implement CRUD Functionality
C#
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#
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:
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.
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.
C#
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.
CSHTML
<p>
<a asp-action="Create">Create New</a>
</p>
<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.
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; }
this.AddRange(items);
}
public bool HasPreviousPage => PageIndex > 1;
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.
C#
ViewData["CurrentFilter"] = searchString;
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#
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>
<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
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>
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 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.
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; }
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;
C#
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.
CSHTML
@model
IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>
@{
ViewData["Title"] = "Student Body Statistics";
}
Run the app and go to the About page. The count of students for each enrollment date
is displayed in a table.
Next steps
In this tutorial, you:
Advance to the next tutorial to learn how to handle data model changes by using
migrations.
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.
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
In Solution Explorer, right-click the project and choose Open Folder in File
Explorer from the context menu.
.NET CLI
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.
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);
});
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.
dotnet ef migrations remove fails, use dotnet ef migrations remove -v to get more
See EF Core Migrations in Team Environments for more information about how the
snapshot file is used.
.NET CLI
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])
);
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.
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).
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.
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:
Prerequisites
Using EF Core migrations
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; }
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.
C#
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.
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.
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; }
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
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 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.
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; }
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
.NET CLI
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.
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;
}
}
The Required attribute must be used with MinimumLength for the MinimumLength to be
enforced.
C#
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; }
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)]
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#
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; }
}
}
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.
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.
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; }
[Range(0, 5)]
public int Credits { 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.
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.
C#
A course can have any number of students enrolled in it, so the Enrollments navigation
property is a collection:
C#
C#
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; }
[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; }
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.
C#
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
C#
7 Note
C#
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
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
}
An enrollment record is for a single course, so there's a CourseID foreign key property
and a Course navigation property:
C#
An enrollment record is for a single student, so there's a StudentID foreign key property
and a Student navigation property:
C#
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.
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; }
}
}
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.
C#
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
This code adds the new entities and configures the CourseAssignment entity's
composite primary key.
C#
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.
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.
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();
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
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.
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.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.
JSON
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;
MultipleActiveResultSets=true"
},
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
.NET CLI
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.
Next steps
In this tutorial, you:
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:
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 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#
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>
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
Run the app and select the Courses tab to see the list with department names.
The list of instructors displays related data from the OfficeAssignment entity. The
Instructor and OfficeAssignment entities are in a one-to-zero-or-one
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.
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; }
}
}
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#
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#
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#
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#
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#
Instead of:
C#
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.
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>
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
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
</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
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#
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.
Next steps
In this tutorial, you:
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:
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#
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#
C#
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
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);
}
C#
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#
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.
C#
return View(course);
}
C#
return View(course);
}
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.
@model ContosoUniversity.Models.Course
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<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.
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#
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();
}
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);
}
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
C#
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i =>
i.OfficeAssignment))
C#
if
(String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Locatio
n))
{
instructorToUpdate.OfficeAssignment = null;
}
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.
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#
The code adds eager loading for the Courses navigation property and calls the new
PopulateAssignedCourseData method to provide information for the checkbox array
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();
}
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;
}
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#
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#
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#
if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}
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;
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.
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);
_context.Instructors.Remove(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
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.
C#
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.
C#
C#
If you modify the CourseAssignments property in this way, you can remove the explicit
property initialization code in the controller.
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;
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.
Next steps
In this tutorial, you:
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:
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.
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.
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.
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.
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.
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; }
[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; }
[Timestamp]
public byte[] RowVersion { 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
.NET CLI
C#
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.
C#
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();
}
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}");
}
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#
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#
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#
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");
Add a hidden field to save the RowVersion property value, immediately following
the hidden field for the DepartmentID property.
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");}
}
C#
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#
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.
CSHTML
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>
<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>
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.
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>
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");}
}
Next steps
In this tutorial, you:
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.
Prerequisites
Handle Concurrency
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
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
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.
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;
}
}
}
}
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; }
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; }
C#
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
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.
.NET CLI
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.
C#
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);
}
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:
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.
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.)
.NET CLI
(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.
Next steps
In this tutorial, you:
Advance to the next tutorial to learn how to handle a variety of relatively advanced
Entity Framework scenarios.
Prerequisites
Implement Inheritance
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.
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.
C#
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.
C#
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);
}
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:
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 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 .
CSHTML
@{
ViewBag.Title = "UpdateCourseCredits";
}
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:
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.
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.
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.
Entity Framework Core implements an in-memory database provider that can be used
for testing. For more information, see Test with InMemory.
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;
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.
C#
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
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.
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.
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.
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
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.
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:
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:
Razor Pages
MVC controllers with views
Web API with controllers
Minimal web APIs
C#
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
C#
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);
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("RPMovieConte
xt")));
The following code uses constructor injection to resolve the database context and
logger from DI:
C#
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.
following code:
C#
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
Host
On startup, an ASP.NET Core app builds a host. The host encapsulates all of the app's
resources, such as:
There are three different hosts capable of running an ASP.NET Core app:
C#
var builder = WebApplication.CreateBuilder(args);
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
For managing confidential configuration data such as passwords, .NET Core provides the
Secret Manager. For production secrets, we recommend Azure Key Vault.
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#
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
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
C#
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#
builder.Services.AddRazorPages();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
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 Make HTTP requests using IHttpClientFactory in ASP.NET
Core.
Content root
The content root is the base path for:
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.
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.
Razor Pages
MVC controllers with views
Web API with controllers
Minimal APIs
C#
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
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.
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.
C#
if (!string.IsNullOrWhiteSpace(option))
{
httpContext.Items["option"] = WebUtility.HtmlEncode(option);
}
await _next(httpContext);
}
}
C#
namespace WebStartup.Middleware;
// <snippet1>
public class RequestSetOptionsStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder>
next)
{
return builder =>
{
builder.UseMiddleware<RequestSetOptionsMiddleware>();
next(builder);
};
}
}
// </snippet1>
C#
using WebStartup.Middleware;
builder.Services.AddRazorPages();
builder.Services.AddTransient<IStartupFilter,
RequestSetOptionsStartupFilter>();
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>
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 .
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.
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.
C#
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#
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:
In the sample app , the IMyDependency interface defines the WriteMessage method:
C#
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;
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();
In the sample app, the IMyDependency service is requested and used to call the
WriteMessage method:
C#
The implementation of the IMyDependency interface can be improved by using the built-
in logging API:
C#
C#
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency2>();
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.
C#
Using the preceding code, there is no need to update Program.cs , because logging is
provided by the framework.
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 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();
C#
using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
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();
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;
}
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;
builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();
builder.Services.AddRazorPages();
var app = builder.Build();
Service lifetimes
See Service lifetimes in Dependency injection in .NET
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.
It's common to use multiple implementations when mocking types for testing.
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>();
C#
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#
The following code creates multiple registrations of the Operation class according to the
named lifetimes:
C#
builder.Services.AddRazorPages();
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
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#
C#
await _next(context);
}
}
C#
await _next(context);
}
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"
}
}
}
C#
builder.Services.AddScoped<IMyDependency, MyDependency>();
app.Run();
Scope validation
See Constructor injection behavior in Dependency injection in .NET
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
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;
Console.WriteLine("Service1.Dispose");
_disposed = true;
}
}
Console.WriteLine("Service2.Dispose");
_disposed = true;
}
}
Console.WriteLine("Service3.Dispose");
_disposed = true;
}
}
C#
using DIsample2.Services;
builder.Services.AddRazorPages();
builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();
C#
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
C#
builder.Services.AddRazorPages();
builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());
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#
...
}
}
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.
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
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.
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#
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.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#
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.
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.
C#
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;
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();
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();
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
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
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();
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#
app.UseRouting();
app.UseResponseCompression();
app.MapRazorPages();
For information about Single Page Applications, see the guides for the React and
Angular project templates.
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run();
The following table shows the requests and responses from http://localhost:1234
using the preceding code.
Request Response
When Map is used, the matched path segments are removed from HttpRequest.Path
and appended to HttpRequest.PathBase for each request.
C#
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});
C#
app.Map("/map1/seg1", HandleMultiSeg);
app.Run();
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#
app.Run();
The following table shows the requests and responses from http://localhost:1234
using the previous code:
Request Response
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#
app.Run();
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.
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 .
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.
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.
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)
URL Rewrite Provides support for Before components that consume the URL.
rewriting URLs and
redirecting 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
Fixed window
Sliding window
Token bucket
Concurrency
C#
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
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();
app.Run();
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;
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();
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 .
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).
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.
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:
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
C#
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
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;
}));
app.UseRateLimiter();
app.Run();
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
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
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;
}));
app.UseRateLimiter();
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.
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: concurrencyPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
app.UseRateLimiter();
}).RequireRateLimiting(concurrencyPolicy);
app.Run();
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 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)
});
}));
});
app.Run();
C#
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
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;
}));
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;
}));
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);
app.Run();
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) called in
Program.cs :
C#
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy) .
C#
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
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;
}));
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;
}));
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();
C#
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
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.
C#
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();
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));
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);
});
});
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();
app.Run();
2 Warning
C#
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using WebRateLimitAuth.Models;
namespace WebRateLimitAuth;
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 .
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();
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
});
}
app.Run();
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#
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
});
}
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
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
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.UseRouting();
C#
app.UseRouting();
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
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.
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 .
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();
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();
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
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();
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:
dotnet/aspnetcore#21677
dotnet/aspnetcore#18463
dotnet/aspnetcore#13273
Response Caching Middleware in
ASP.NET Core
Article • 04/11/2023
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#
builder.Services.AddResponseCaching();
app.UseResponseCaching();
2 Warning
C#
builder.Services.AddResponseCaching();
app.UseHttpsRedirection();
app.UseResponseCaching();
await next();
});
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
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).
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#
builder.Services.AddResponseCaching(options =>
{
options.MaximumBodySize = 1024;
options.UseCaseSensitivePaths = true;
});
app.UseHttpsRedirection();
app.UseResponseCaching();
await next(context);
});
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.
Header Details
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‡
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.
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.
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
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;
app.UseHttpsRedirection();
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
app.Run();
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.
For ASP.NET Core's built-in localization support, see Globalization and localization in
ASP.NET Core.
C#
using System.Globalization;
namespace Middleware.Example;
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
Additional parameters for the constructor and Invoke / InvokeAsync are populated by
dependency injection (DI).
C#
using System.Globalization;
namespace Middleware.Example;
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
C#
using Middleware.Example;
using System.Globalization;
app.UseHttpsRedirection();
app.UseRequestCulture();
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.
C#
namespace Middleware.Example;
Lifetime and registration options contains a complete sample of middleware with scoped
lifetime services.
C#
using Middleware.Example;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMessageWriter, LoggingMessageWriter>();
app.UseMyCustomMiddleware();
app.Run();
C#
namespace Middleware.Example;
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
C#
while (true)
{
var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0,
buffer.Length);
if (bytesRemaining == 0)
{
break;
}
ArrayPool<byte>.Shared.Return(buffer);
If you would like to see code comments translated to languages other than English, let
us know in this GitHub discussion issue .
2 Warning
C#
while (true)
{
var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0,
buffer.Length);
if (bytesRemaining == 0)
{
results.Add(builder.ToString());
break;
}
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);
}
ArrayPool<byte>.Shared.Return(buffer);
return results;
}
Doesn't buffer the entire request body in a StringBuilder unless there aren't any
newline characters.
Doesn't call Split on the string.
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;
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);
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;
}
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
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
When the Content-Encoding header value on a request matches one of the available
decompression providers, the middleware:
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.
Configuration
The following code shows how to enable request decompression for the default
Content-Encoding types:
C#
builder.Services.AddRequestDecompression();
app.UseRequestDecompression();
app.Run();
C#
C#
builder.Services.AddRequestDecompression(options =>
{
options.DecompressionProviders.Add("custom", new
CustomDecompressionProvider());
});
app.UseRequestDecompression();
app.Run();
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.
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
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.
C#
if (!string.IsNullOrWhiteSpace(keyValue))
{
dbContext.Requests.Add(new Request("Conventional", keyValue));
await dbContext.SaveChangesAsync();
}
await _next(context);
}
}
C#
if (!string.IsNullOrWhiteSpace(keyValue))
{
_dbContext.Requests.Add(new Request("Factory", keyValue));
await _dbContext.SaveChangesAsync();
}
await next(context);
}
}
C#
C#
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#
app.UseConventionalMiddleware();
app.UseFactoryActivatedMiddleware();
IMiddlewareFactory
IMiddlewareFactory provides methods to create middleware. The middleware factory
implementation is registered in the container as a scoped service.
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
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.
C#
IMiddleware
IMiddleware defines middleware for the app's request pipeline.
C#
if (!string.IsNullOrWhiteSpace(keyValue))
{
_db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation =
"SimpleInjectorActivatedMiddleware",
Value = keyValue
});
await _db.SaveChangesAsync();
}
await next(context);
}
}
C#
C#
_container.Register<AppDbContext>(() =>
{
var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
optionsBuilder.UseInMemoryDatabase("InMemoryDb");
return new AppDbContext(optionsBuilder.Options);
}, Lifestyle.Scoped);
_container.Register<SimpleInjectorActivatedMiddleware>();
_container.Verify();
}
C#
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#
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.
C#
app.Run();
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
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.UseRouting();
C#
app.UseRouting();
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
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#
app.Run("http://localhost:3000");
Multiple ports
In the following code, the app responds to port 3000 and 4000 .
C#
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.Run();
.NET CLI
C#
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.
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
http://*:3000
C#
app.Urls.Add("http://*:3000");
app.Run();
http://+:3000
C#
app.Urls.Add("http://+:3000");
app.Run();
http://0.0.0.0:3000
C#
app.Urls.Add("http://0.0.0.0:3000");
app.Run();
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
app.Urls.Add("https://localhost:3000");
app.Run();
For more information on the development certificate, see Trust the ASP.NET Core HTTPS
development certificate on Windows and macOS.
JSON
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
C#
app.Urls.Add("https://localhost:3000");
app.Run();
C#
using System.Security.Cryptography.X509Certificates;
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);
});
});
app.Urls.Add("https://localhost:3000");
app.Run();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
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#
app.Run();
Logging
The following code writes a message to the log on application startup:
C#
app.Run();
For more information, see Logging in .NET Core and ASP.NET Core
C#
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
app.MapControllers();
app.Run();
WebApplicationBuilder
This section contains sample code using WebApplicationBuilder.
C#
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}");
C#
builder.Configuration.AddIniFile("appsettings.ini");
Read configuration
By default the WebApplicationBuilder reads configuration from multiple sources,
including:
Environment variables
The command line
C#
app.Run();
C#
if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}
app.Run();
app.Run();
Add services
C#
C#
app.Run();
C#
app.Run();
C#
app.Run();
C#
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
Add Middleware
Any existing ASP.NET Core middleware can be configured on the WebApplication :
C#
app.Run();
C#
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.
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:
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();
appsettings.{Environment}.json .
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
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.
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.
C#
public class HostApplicationLifetimeEventsHostedService : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public HostApplicationLifetimeEventsHostedService(
IHostApplicationLifetime hostApplicationLifetime)
=> _hostApplicationLifetime = hostApplicationLifetime;
return Task.CompletedTask;
}
IHostLifetime
The IHostLifetime implementation controls when the host starts and when it stops. The
last implementation registered is used.
ApplicationName
EnvironmentName
ContentRootPath
Host configuration
Host configuration is used for the properties of the IHostEnvironment implementation.
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.
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
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
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")
// ...
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);
});
});
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.
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
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
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
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
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
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
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
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
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
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");
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
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#
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.
C#
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddXmlFile("appsettings.xml", optional: true,
reloadOnChange: true);
})
...
C#
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Warning);
})
...
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.
7 Note
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.
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")
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>")
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
C#
WebHost.CreateDefaultBuilder(args)
.UseEnvironment(EnvironmentName.Development)
Key: hostingStartupAssemblies
Type: string
Default: Empty string
Set using: UseSetting
Environment variable: ASPNETCORE_HOSTINGSTARTUPASSEMBLIES
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")
Key: hostingStartupExcludeAssemblies
Type: string
Default: Empty string
Set using: UseSetting
Environment variable: ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES
C#
WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey,
"assembly1;assembly2")
Key: preferHostingUrls
Type: bool ( true or 1 )
Default: true
Set using: PreferHostingUrls
Environment variable: ASPNETCORE_PREFERHOSTINGURLS
C#
WebHost.CreateDefaultBuilder(args)
.PreferHostingUrls(false)
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
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
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")
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
C#
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
The Run method starts the web app and blocks the calling thread until the host is shut
down:
C#
host.Run();
Start
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#
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)
C#
C#
Produces the same result as Start(RequestDelegate app), except the app responds on
http://localhost:8080 .
Start(Action<IRouteBuilder> routeBuilder)
C#
Request Response
C#
StartWith(Action<IApplicationBuilder> app)
C#
C#
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#
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#
7 Note
The IWebHostEnvironment service can also be injected directly into the Configure
method for setting up the processing pipeline:
C#
IWebHostEnvironment can be injected into the Invoke method when creating custom
middleware:
C#
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.
C#
StopApplication to gracefully shut down an app when the class's Shutdown method is
called:
C#
public class MyClass
{
private readonly IHostApplicationLifetime _appLifetime;
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.
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
Application configuration is the highest priority and is detailed in the next section. Host
configuration follows application configuration, and is described in this article.
C#
For the .NET Generic Host and Web Host, the default host configuration sources from
highest to lowest priority is:
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
C#
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.
appsettings.json
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#
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.
by default:
If a configuration value must be guaranteed, see GetValue. The preceding example only
reads strings and doesn’t support a default value.
Comments in appsettings.json
Comments in appsettings.json and appsettings.{Environment}.json files are supported
using JavaScript or C# style comments.
JSON
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
C#
An options class:
C#
public class Test22Model : PageModel
{
private readonly IConfiguration Configuration;
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);
In the preceding code, by default, changes to the JSON configuration file after the app
has started are read.
C#
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;
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
Using the preceding code, the following code reads the position options:
C#
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.
See JSON configuration provider in this document for information on adding additional
JSON configuration files.
C#
using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
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>();
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;
}
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;
builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();
builder.Services.AddRazorPages();
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.
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:
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
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
C#
builder.Services.AddRazorPages();
builder.Configuration.AddEnvironmentVariables(prefix: "MyCustomPrefix_");
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.
.NET CLI
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:
For more information, see Azure Apps: Override app configuration using the Azure
Portal.
See Connection string prefixes for information on Azure database connection strings.
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
JSON
"applicationUrl": "https://localhost:5001;http://localhost:5000"
C#
Command-line
Using the default configuration, the CommandLineConfigurationProvider loads
configuration from command-line argument key-value pairs after the following
configuration sources:
Command-line arguments
The following command sets keys and values using = :
.NET CLI
.NET CLI
.NET CLI
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 ( - ).
C#
builder.Configuration.AddCommandLine(args, switchMappings);
.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#
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#
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 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.
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.
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.
MYSQLCONNSTR_ MySQL
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).
C#
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();
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#
C#
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
builder.Configuration.AddJsonFile("MyConfig.json",
optional: true,
reloadOnChange: true);
builder.Services.AddRazorPages();
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.
C#
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();
XML
The following code from the sample download displays several of the preceding
configurations settings:
C#
Repeating elements that use the same element name work if the name attribute is used
to distinguish the elements:
XML
The following code reads the previous configuration file and displays the keys and
values:
C#
XML
The previous configuration file loads the following keys with value :
key:attribute
section:key:attribute
C#
C#
builder.Configuration.AddInMemoryCollection(Dict);
builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddCommandLine(args);
builder.Services.AddRazorPages();
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;
UseUrls
--urls on the command line
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:
Kestrel binds to the endpoint configured specifically for Kestrel in the appsettings.json
file ( https://localhost:9999 ) and not https://localhost:7777 .
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#
In the preceding code, if NumberKey isn't found in the configuration, the default value of
99 is used.
JSON
{
"section0": {
"key0": "value00",
"key1": "value01"
},
"section1": {
"key0": "value10",
"key1": "value11"
},
"section2": {
"subsection0": {
"key0": "value200",
"key1": "value201"
},
"subsection1": {
"key0": "value210",
"key1": "value211"
}
}
}
C#
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("MySubsection.json",
optional: true,
reloadOnChange: true);
builder.Services.AddRazorPages();
GetSection
IConfiguration.GetSection returns a configuration subsection with the specified
subsection key.
C#
C#
When GetSection returns a matching section, Value isn't populated. A Key and Path are
returned when the section exists.
C#
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.
JSON
{
"array": {
"entries": {
"0": "value00",
"1": "value10",
"2": "value20",
"4": "value40",
"5": "value50"
}
}
}
C#
builder.Configuration
.AddJsonFile("MyArray.json",
optional: true,
reloadOnChange: true);
builder.Services.AddRazorPages();
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; }
return Content(s);
}
}
C#
text
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.
Models/EFConfigurationValue.cs :
C#
EFConfigurationProvider/EFConfigurationContext.cs :
C#
EFConfigurationProvider/EFConfigurationSource.cs :
C#
public EFConfigurationSource(Action<DbContextOptionsBuilder>
optionsAction) => _optionsAction = optionsAction;
EFConfigurationProvider/EFConfigurationProvider.cs :
C#
OptionsAction(builder);
dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());
dbContext.SaveChanges();
return configValues;
}
}
Extensions/EntityFrameworkExtensions.cs :
C#
C#
//using Microsoft.EntityFrameworkCore;
builder.Configuration.AddEFConfiguration(
opt => opt.UseInMemoryDatabase("InMemoryDb"));
app.Run();
C#
// ...
}
}
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
In the following code, MyOptions is added to the service container with Configure and
bound to configuration:
C#
using SampleApp.Models;
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
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>
CSHTML
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
C#
app.Logger.LogInformation($"KeyOne = {key1}");
app.Logger.LogInformation($"KeyTwo = {key2}");
app.Run();
C#
using SampleApp.Models;
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(myOptions =>
{
myOptions.Option1 = "Value configured in delegate";
myOptions.Option2 = 500;
});
C#
In the preceding example, the values of Option1 and Option2 are specified in
appsettings.json and then overridden by the configured delegate.
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:
For more information on migrating app configuration from earlier versions of ASP.NET,
see Update from ASP.NET to 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
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.
JSON
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
C#
An options class:
C#
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);
In the preceding code, by default, changes to the JSON configuration file after the app
has started are read.
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;
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
Using the preceding code, the following code reads the position options:
C#
public class Test2Model : PageModel
{
private readonly PositionOptions _options;
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>:
IOptionsSnapshot<TOptions>:
IOptionsMonitor<TOptions>:
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.
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.
C#
The following code registers a configuration instance which MyOptions binds against:
C#
using SampleApp.Models;
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
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;
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
C#
In the preceding code, by default, changes to the JSON configuration file after the app
has started are read.
Are useful when multiple configuration sections bind to the same properties.
Are case sensitive.
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#
C#
using SampleApp.Models;
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"));
C#
public TestNOModel(IOptionsSnapshot<TopItemSettings>
namedOptionsAccessor)
{
_monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
_yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
}
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 .
See Use AddOptions to configure custom repository for information adding a custom
repository.
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.
Options validation
Options validation enables option values to be validated.
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#
[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; }
}
C#
using OptionsValidationSample.Configuration;
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();
The following code displays the configuration values or the validation errors:
C#
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;
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.
C#
if (string.IsNullOrEmpty(match.Value))
{
vor = $"{options.Key1} doesn't match RegEx \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>();
Implement the IValidatableObject interface and its Validate method within the
class.
Call ValidateDataAnnotations in Program.cs .
ValidateOnStart
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;
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
C#
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"));
C#
using OptionsValidationSample.Configuration;
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#
Additional resources
View or download sample code (how to download)
Use multiple environments in ASP.NET
Core
Article • 04/11/2023
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
Staging
Production: The default if DOTNET_ENVIRONMENT and ASPNETCORE_ENVIRONMENT have
not been set.
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#
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
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
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
.NET CLI
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
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"
}
}
}
}
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,
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
The value of commandName can specify the web server to launch. commandName can be any
one of the following:
IISExpress : Launches IIS Express.
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.
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"
}
}
}
}
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
2 Warning
launchSettings.json shouldn't store secrets. The Secret Manager tool can be used
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.
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.
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 .
To set the environment in an Azure App Service app by using the portal:
Azure App Service automatically restarts the app after an app setting is added, changed,
or deleted in the Azure portal.
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
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
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.
XML
<PropertyGroup>
<EnvironmentName>Development</EnvironmentName>
</PropertyGroup>
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
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.
C#
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = Environments.Staging
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Configuration by environment
To load configuration by environment, see Configuration in ASP.NET Core.
C#
var builder = WebApplication.CreateBuilder(args);
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
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.
C#
builder.Services.AddRazorPages();
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#
builder.Services.AddRazorPages();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
C#
Create logs
To create logs, use an ILogger<TCategoryName> object from dependency injection (DI).
C#
Levels and categories are explained in more detail later in this document.
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"
}
}
}
"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
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.
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.
}
}
}
}
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 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"
}
}
}
}
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.
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#
The following example calls AddConsole in Program.cs and logs the /Test endpoint:
C#
var builder = WebApplication.CreateBuilder(args);
app.Run();
C#
using Microsoft.Extensions.Logging.Console;
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:
.NET CLI
set Logging__LogLevel__Microsoft=Information
dotnet run
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
JSON
"Logging": {
"Console": {
"LogLevel": {
"Microsoft.Hosting.Lifetime": "Trace"
}
}
}
Console
On Azure App Service , select New application setting on the Settings >
Configuration page. Azure App Service application settings are:
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.
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 .
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#
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:
Information 2 LogInformation Tracks the general flow of the app. May have long-term
value.
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.
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);
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.
C#
[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
_logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", 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:
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
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#
C#
[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
_logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", 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.
C#
[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
_logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", 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;
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
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();
}
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#
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#
The preceding code displays console logs when the category contains Controller or
Microsoft and the log level is Information or higher.
Category Notes
Microsoft.AspNetCore.Hosting How long HTTP requests took to complete and what time
they started. Which hosting startup assemblies were
loaded.
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:
Console
AzureAppServicesFile and AzureAppServicesBlob
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);
if (todoItem == null)
{
_logger.LogWarning(MyLogEvents.GetItemNotFound,
"Get({Id}) NOT FOUND", id);
return NotFound();
}
}
return ItemToDTO(todoItem);
}
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.
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.
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.
.NET CLI
dotnet trace ps
Find the PID for the process that has the same name as the app's assembly.
.NET CLI
When using a PowerShell command shell, enclose the --providers value in single
quotes ( ' ):
.NET CLI
Keyword Description
1 Log meta events about the LoggingEventSource . Doesn't log events from
ILogger .
0 LogAlways
1 Critical
2 Error
3 Warning
4 Informational
5 Verbose
Trace 0
Debug 1
Category named value Numeric value
Information 2
Warning 3
Error 4
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.
.NET CLI
.NET CLI
.NET CLI
.NET CLI
.NET CLI
( 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.
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"
}
}
}
LogName : "Application"
The following code changes the SourceName from the default value of ".NET Runtime" to
MyLogs :
C#
The provider package isn't included in the shared framework. To use the provider, add
the provider package to the project.
C#
using Microsoft.Extensions.Logging.AzureAppServices;
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.
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 supports viewing log activity in real time from:
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.
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:
For more information, see each provider's documentation. Third-party logging providers
aren't supported by Microsoft.
C#
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Debug;
level Debug . The filter is applied to all providers because a specific provider was not
configured.
C#
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 .
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 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.
C#
app.UseHttpLogging();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
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"
C#
using Microsoft.AspNetCore.HttpLogging;
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();
await next();
});
app.Run();
7 Note
LoggingFields
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;
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;
});
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseHttpLogging();
await next();
});
app.Run();
MediaTypeOptions
C#
using Microsoft.AspNetCore.HttpLogging;
});
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseHttpLogging();
await next();
});
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
ResponseBodyLogLimit
C#
using Microsoft.AspNetCore.HttpLogging;
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;
});
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseHttpLogging();
await next();
});
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:
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
Enable W3CLogger
Enable W3CLogger with UseW3CLogging, which adds the W3CLogger middleware:
C#
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#
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
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.
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#
app.Run();
C#
app.Run();
HttpRequest.Body can be read directly or used with other APIs that accept stream.
7 Note
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.
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#
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.
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#
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:
C#
app.Run();
HttpResponse.Body can be written directly or used with other APIs that write to a stream.
BodyWriter
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.
C#
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#
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.
7 Note
C#
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#
app.Run();
7 Note
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.
C#
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;
builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
// 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>();
if (!httpResponseMessage.IsSuccessStatusCode)
return Results.BadRequest();
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;
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>();
if (!httpResponseMessage.IsSuccessStatusCode)
return Results.BadRequest();
return Results.Ok(response);
});
app.Run();
The UserAgentHeaderHandler :
C#
using Microsoft.Net.Http.Headers;
namespace HttpContextInBackgroundThread;
if (string.IsNullOrEmpty(userAgentString))
{
userAgentString = "Unknown";
}
request.Headers.Add(HeaderNames.UserAgent, userAgentString);
_logger.LogInformation($"User-Agent: {userAgentString}");
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
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.
C#
using System.Text.Json;
namespace HttpContextInBackgroundThread;
public PeriodicBranchesLoggerService(IHttpClientFactory
httpClientFactory,
ILogger<PeriodicBranchesLoggerService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
}
stoppingToken);
if (httpResponseMessage.IsSuccessStatusCode)
{
await using var contentStream =
await
httpResponseMessage.Content.ReadAsStreamAsync(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!");
}
}
}
C#
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;
builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
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.
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:
Routing basics
The following code shows a basic example of routing:
C#
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.
C#
app.UseRouting();
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:
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:
The following example shows routing with a more sophisticated route template:
C#
The following example shows routing with health checks and authorization:
C#
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
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:
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.
The following code shows how to retrieve and inspect the endpoint matching the
current request:
C#
if (currentEndpoint is null)
{
await next(context);
return;
}
Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");
await next(context);
});
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.
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();
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.
txt
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
txt
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
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 following code demonstrates how middleware can influence or react to routing:
C#
app.UseHttpMethodOverride();
app.UseRouting();
await next(context);
});
C#
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:
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.
C#
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:
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:
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.
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:
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
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 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} :
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 details of how precedence works are coupled to how route templates are defined:
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.
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.
GetPathByAddress Generates a URI with an absolute path based on the provided values.
2 Warning
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#
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 }} .
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.
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:
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.
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
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 .
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 .
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.
C#
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
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:
2 Warning
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.
2 Warning
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.
C#
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
() => "Inline Regex Constraint Matched");
C#
app.MapControllerRoute(
name: "people",
pattern: "people/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List" });
^\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:
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 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)));
C#
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
[HttpGet("{id:noZeroes}")]
public IActionResult Get(string id) =>
Content(id);
}
if (routeValueString is null)
{
return false;
}
return _regex.IsMatch(routeValueString);
}
}
2 Warning
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:
Parameter transformers
Parameter transformers:
C#
return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100))
.ToLowerInvariant();
}
}
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?}");
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.
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.
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:
The role of the address scheme is to make the association between the address and
matching endpoints by arbitrary criteria:
The following example shows ambient values and explicit values. It provides ambient
values from the current request and explicit values:
C#
return Content(indexPath);
}
// ...
Returns /Widget/Index/17
Gets LinkGenerator via DI.
The following code provides only explicit values and no ambient values:
C#
C#
The following code provides the controller from ambient values in the current request
and explicit values:
C#
/Gadget/Edit/17 is returned.
The following code provides ambient values from the current request and explicit
values:
C#
// ...
}
}
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
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.
compatibility.
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 :
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
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 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?} .
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 ?? ""}");
}
}
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 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.
The following code uses RequireHost to require the specified host on the route:
C#
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:
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;
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#
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#
C#
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.
.NET CLI
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#
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms",
stopwatch.ElapsedMilliseconds);
});
app.UseRouting();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms",
stopwatch.ElapsedMilliseconds);
});
app.UseAuthorization();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms",
stopwatch.ElapsedMilliseconds);
});
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#
_logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
_message, _stopwatch.ElapsedMilliseconds);
_disposed = true;
}
}
C#
app.UseRouting();
app.UseAuthorization();
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.
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.
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 .
C#
// Your framework
app.MapMyFramework(...);
app.MapHealthChecks("/healthz");
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");
C#
[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:
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:
The metadata collection is ordered and supports overriding by priority. In the case of
controllers, metadata on the action method is most specific.
C#
// Your framework
app.MapMyFramework(...).RequireAuthorization();
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.
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.
2 Warning
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.
C#
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.
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.
C#
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.
C#
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode =
StatusCodes.Status500InternalServerError;
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#
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages();
Console
7 Note
The status code pages middleware does not catch exceptions. To provide a custom
error handling page, use the exception handler page.
C#
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
C#
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page:
{statusCodeContext.HttpContext.Response.StatusCode}");
});
UseStatusCodePagesWithRedirects
The UseStatusCodePagesWithRedirects extension method:
C#
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
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:
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#
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.
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#
var statusCodeReExecuteFeature =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
To disable status code pages for specific requests in a Razor Pages handler method or in
an MVC controller, use IStatusCodePagesFeature:
C#
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 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:
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.
C#
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.
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.
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 ):
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.
1. Use ProblemDetailsOptions.CustomizeProblemDetails
2. Use a custom IProblemDetailsWriter
3. Call the IProblemDetailsService in a middleware
CustomizeProblemDetails operation
C#
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
C#
C#
builder.Services.AddTransient<IProblemDetailsWriter,
SampleProblemDetailsWriter>();
var app = builder.Build();
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();
}
// /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();
C#
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
app.UseHttpsRedirection();
app.UseStatusCodePages();
// /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();
}
// /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.
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();
}
// /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));
}
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
);
}
// /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));
}
C#
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
app.UseStatusCodePages();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapControllers();
app.Run();
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.
C#
using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
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;
if (context.RequestServices.GetService<IProblemDetailsService>()
is
{ } problemDetailsService)
{
var exceptionHandlerFeature =
context.Features.Get<IExceptionHandlerFeature>();
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.
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
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
Basic usage
Register IHttpClientFactory by calling AddHttpClient in Program.cs :
C#
C#
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
Named clients
Named clients are a good choice when:
C#
// 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");
});
CreateClient
C#
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
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.
C#
// 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");
}
API-specific methods can be created that expose HttpClient functionality. For example,
the GetAspNetCoreDocsBranches method encapsulates code to retrieve docs GitHub
branches.
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:
C#
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.
C#
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");
});
C#
POST
PUT
DELETE
PATCH
C#
httpResponseMessage.EnsureSuccessStatusCode();
}
HttpClient also supports other types of content. For example, MultipartContent and
StreamContent. For a complete list of supported content, see HttpContent.
C#
public async Task SaveItemAsync(TodoItem todoItem)
{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem),
Encoding.UTF8,
Application.Json);
httpResponseMessage.EnsureSuccessStatusCode();
}
The preceding code is similar to the POST example. The SaveItemAsync method calls
PutAsync instead of PostAsync .
C#
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.
C#
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>();
For example, consider the following interface and its implementation, which represents a
task as an operation with an identifier, OperationId :
C#
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#
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:
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
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.
C#
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)));
C#
policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);
builder.Services.AddHttpClient("PollyRegistryRegular")
.AddPolicyHandlerFromRegistry("Regular");
builder.Services.AddHttpClient("PollyRegistryLong")
.AddPolicyHandlerFromRegistry("Long");
For more information on IHttpClientFactory and Polly integrations, see the Polly wiki .
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.
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.
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:
C#
builder.Services.AddHttpClient("NoAutomaticCookies")
.ConfigurePrimaryHttpMessageHandler(() =>
new HttpClientHandler
{
UseCookies = false
});
Microsoft.Extensions.Hosting
Microsoft.Extensions.Http
IHttpClientFactory .
GitHubService uses IHttpClientFactory to create an instance of HttpClient , which
C#
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
try
{
var gitHubService = host.Services.GetRequiredService<GitHubService>();
var gitHubBranches = await
gitHubService.GetAspNetCoreDocsBranchesAsync();
httpResponseMessage.EnsureSuccessStatusCode();
C#
builder.Services.AddHttpClient("PropagateHeaders")
.AddHeaderPropagation();
builder.Services.AddHeaderPropagation(options =>
{
options.Headers.Add("X-TraceId");
});
app.UseHeaderPropagation();
app.MapControllers();
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
Static files, such as HTML, CSS, images, and JavaScript, are assets an ASP.NET Core app
serves directly to clients by default.
The CreateBuilder method sets the content root to the current directory:
C#
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
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
https://localhost:5001/images/MyImage.jpg
C#
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
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
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;
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
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.
HTML
To serve files from multiple locations, see Serve files from multiple locations.
C#
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
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).
C#
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using StaticFileAuth.Data;
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();
});
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.
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;
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.
C#
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.AddDirectoryBrowser();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
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.
C#
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
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.
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.
C#
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
Call app.UseFileServer to enable the serving of static files and the default file. Directory
browsing isn't enabled:
C#
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
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#
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.AddDirectoryBrowser();
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();
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;
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.AddDirectoryBrowser();
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();
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
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;
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
The following code enables serving unknown types and renders the unknown file as an
image:
C#
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
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
CSHTML
@page
C#
C#
app.UseStaticFiles();
2 Warning
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:
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.
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.
With the following updated Program.cs file that sets WebRootPath = "wwwroot-
custom" :
C#
app.UseDefaultFiles();
app.UseStaticFiles();
app.Run();
In the preceding code, requests to / :
To ensure assets from wwwroot-custom are returned, use one of the following
approaches:
XML
<ItemGroup>
<Content Remove="wwwroot\**" />
</ItemGroup>
C#
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
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.
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.
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.
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.
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:
Using .NET for client-side web development offers the following advantages:
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.
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.
Benefits of ASP.NET Core SPA with JavaScript Frameworks, in addition to the client
rendering benefits previously listed:
Downsides:
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:
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.
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#
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();
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>
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#";
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:
/Pages/Index.cshtml / or /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
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.
C#
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddDbContext<CustomerDbContext>(options =>
options.UseInMemoryDatabase("name"));
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
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)
{
}
CSHTML
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<form method="post">
Name:
<input asp-for="Customer!.Name" />
<input type="submit" />
</form>
C#
public class CreateModel : PageModel
{
private readonly Data.CustomerDbContext _context;
[BindProperty]
public Customer? Customer { get; set; }
return RedirectToPage("./Index");
}
}
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:
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
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.
C#
[BindProperty]
public Customer? Customer { get; set; }
return RedirectToPage("./Index");
}
CSHTML
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
HTML
<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>
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.
C#
[BindProperty]
public Customer? Customer { get; set; }
return RedirectToPage("./Index");
}
C#
[BindProperty]
public Customer? Customer { get; set; }
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.
C#
[BindProperty(SupportsGet = true)]
For more information, see ASP.NET Core Community Standup: Bind on GET
discussion (YouTube) .
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.
CSHTML
@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
C#
if (contact != null)
{
_context.Customer.Remove(contact);
await _context.SaveChangesAsync();
}
return RedirectToPage();
}
}
CSHTML
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
The Index.cshtml file contains markup to create a delete button for each customer
contact:
CSHTML
HTML
When the delete button is rendered in HTML, its formaction includes parameters for:
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#
if (contact != null)
{
_context.Customer.Remove(contact);
await _context.SaveChangesAsync();
}
return RedirectToPage();
}
The OnPostDeleteAsync method:
@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?}"
C#
[BindProperty]
public Customer? Customer { get; set; }
if (Customer == null)
{
return NotFound();
}
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");
}
Validation
Validation rules:
C#
using System.ComponentModel.DataAnnotations;
namespace RazorPagesContacts.Models
{
public class Customer
{
public int Id { get; set; }
[Required, StringLength(10)]
public string? Name { get; set; }
}
}
CSHTML
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<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>
Uses the <div /> and <span /> Tag Helpers to enable:
Client-side validation.
Validation error rendering.
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.
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { 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 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:
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.
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.
HTML
HTML
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:
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>
places the scoped CSS file, and the rest of the app's assets, at the _content path:
XML
<PropertyGroup>
<StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>
XML
<PropertyGroup>
<DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>
{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.
HTML
For information on Blazor CSS isolation, see ASP.NET Core Blazor CSS isolation.
C#
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.
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.
@{
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.
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.
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.
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.
C#
namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
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.
CSHTML
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<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>
CSHTML
@page
@model CreateModel
<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.
For more information on partial views, see Partial views in ASP.NET Core.
C#
[BindProperty]
public Customer Customer { get; set; }
_context.Customers.Add(Customer);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
/Pages
Index.cshtml
Privacy.cshtml
/Customers
Create.cshtml
Edit.cshtml
Index.cshtml
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", ...)
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
Relative name linking is useful when building sites with a complex structure. When
relative names are used to link between pages in a folder:
C#
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#
CSHTML
<h1>@Model.Title</h1>
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.
C#
public class CreateDotModel : PageModel
{
private readonly AppDbContext _db;
[TempData]
public string Message { get; set; }
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";
return RedirectToPage("./Index");
}
}
CSHTML
<h3>Msg: @Model.Message</h3>
C#
[TempData]
public string Message { get; set; }
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
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;
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
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
Using the preceding code, the URL path that submits to OnPostJoinListAsync is
https://localhost:5001/Customers/CreateFATH?handler=JoinList . The URL path that
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}" .
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 .
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 .
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:
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
Blazor example:
JavaScript
razor
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
C#
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"));
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.
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"));
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
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"));
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.
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
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.
Select Create.
The following starter project is created:
For alternative approaches to create the project, see Create a new project in Visual
Studio.
Visual Studio displays the following dialog when a project is not yet configured to
use SSL:
Visual Studio:
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#
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
C#
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#
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.
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
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.
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.
C#
using System.ComponentModel.DataAnnotations;
namespace RazorPagesMovie.Models;
The question mark after string indicates that the property is nullable. For
more information, see Nullable reference types.
Visual Studio
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.
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);
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Visual Studio
In this section, the Package Manager Console (PMC) window is used to:
1. From the Tools menu, select NuGet Package Manager > Package Manager
Console.
PowerShell
Add-Migration InitialCreate
Update-Database
No type was specified for the decimal column 'Price' on entity type 'Movie'. This will
cause values to be silently truncated if they do not fit in the default precision and
scale. Explicitly specify the SQL server column type that can accommodate all the
values using 'HasColumnType()'.
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)
{
}
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 ).
Console
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 .
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);
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
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.
C#
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies;
Razor Pages are derived from PageModel. By convention, the PageModel derived class is
named PageNameModel . For example, the Index page is named IndexModel .
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;
C#
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
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#.
@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.
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.
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.
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.
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
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>
CSHTML
<a class="navbar-brand" asp-area="" asp-
page="/Index">RazorPagesMovie</a>
CSHTML
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.
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.
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;
}
[BindProperty]
public Movie Movie { get; set; } = default!;
_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#
_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.
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">
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
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);
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
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
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
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.
C#
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
namespace RazorPagesMovie.Models;
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;
}
Visual Studio
C#
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
SeedData.Initialize(services);
}
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
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'.
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.
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models;
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
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,
CSHTML
@page "{id:int?}"
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#
if (Movie == null)
{
return NotFound();
}
return Page();
}
C#
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
The previous code detects concurrency exceptions when one client deletes the movie
and the other client posts changes to the movie.
To test the catch block:
Production code may want to detect concurrency conflicts. See Handle concurrency
conflicts for more information.
C#
[BindProperty]
public Movie Movie { get; set; } = default!;
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
When an HTTP GET request is made to the Movies/Edit page, for example,
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
on the page.
The Edit form is displayed with the values from the movie.
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.
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
C#
[BindProperty(SupportsGet = true)]
public string? SearchString { get; set; }
[BindProperty(SupportsGet = true)]
public string? MovieGenre { get; set; }
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
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.
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#
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));
}
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
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.*@
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
Search by genre
Update the Index page's OnGetAsync method with the following code:
C#
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#
C#
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>
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
When using EF Code First to automatically create and track a database, Code First:
Automatic verification that the schema and model are in sync makes it easier to find
inconsistent database code issues.
C#
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>
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 :
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.
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.
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"
},
Visual Studio
PowerShell
Add-Migration Rating
Update-Database
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:
4. Select OK.
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:
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.
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;
[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.
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 .
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.
2. Set a break point in the OnPostAsync method of the Create or Edit page.
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:
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.
C#
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
The [DataType] attributes provide:
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.
Can enable the app to automatically provide type-specific features. For example, a
mailto: link can be created for DataType.EmailAddress .
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 .
C#
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#
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.
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models;
[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#
SQL
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
C#
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)");
}
SQL
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.
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.
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.
C#
"SampleAsyncPageFilter.OnPageHandlerSelectionAsync",
value, key.ToString());
return Task.CompletedTask;
}
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.
C#
C#
C#
C#
C#
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();
}
}
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;
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.
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.
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)
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
0 Order isn't specified (default value). Not assigning Order ( Order = null ) defaults the
route Order to 0 (zero) for processing.
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.
C#
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace SampleApp.Conventions;
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;
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?}"),
}
});
}
});
});
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
C#
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace SampleApp.Conventions;
The Order property for the AttributeRouteModel is set to 1 . This ensures the following
route matching behavior in the sample app:
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.
template used:
.NET CLI
info: SampleApp.Pages.AboutModel[0]
/About/GlobalRouteValue Order = 1 Template =
About/{globalTemplate?}
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#
Program.cs :
C#
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:
C#
C#
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
Wherever possible, don't set the Order , which results in Order = 0 . Rely on routing to
select the correct route.
the result:
C#
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.
.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.
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
For the examples in this section, the sample app uses an AddHeaderAttribute class,
which is a ResultFilterAttribute, that applies a response header:
C#
Using conventions, the sample demonstrates how to apply the attribute to all of the
pages in a folder and to a single page.
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:
C#
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
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#
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 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#
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#
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; }
A controller action:
C#
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
<!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.
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.
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>
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:
Prerequisites
Visual Studio
Visual Studio 2022 with the ASP.NET and web development workload.
Create a web app
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.
Visual Studio
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.
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:
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:
In the next tutorial in this series, you learn about MVC and start writing some code.
By Rick Anderson
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:
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 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.
C#
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers;
An HTTP endpoint:
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
Append /HelloWorld to the path in the address bar. The Index method returns a string.
MVC invokes controller classes, and the action methods within them, depending on the
incoming URL. The default URL routing logic used by MVC, uses a format like this to
determine what code to invoke:
/[Controller]/[ActionName]/[Parameters]
C#
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 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
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}");
}
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.
C#
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
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.
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#
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.
Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the
following:
CSHTML
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
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:
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.
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>
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
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.
@{
Layout = "_Layout";
}
CSHTML
@{
ViewData["Title"] = "Movie List";
}
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
CSHTML
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 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.
Controllers are responsible for providing the data required in order for a view template
to render a response.
Do business logic
Interact with a database directly.
A view template should work only with the data that's provided to it by the controller.
Maintaining this "separation of concerns" helps keep the code:
Clean.
Testable.
Maintainable.
Currently, the Welcome method in the HelloWorldController class takes a name and 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.
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;
The ViewData dictionary object contains data that will be passed to the view.
CSHTML
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4
Data is taken from the URL and passed to the controller using the MVC model binder.
The controller packages the data into a ViewData dictionary and passes that object to
the view. The view then renders the data as HTML to the browser.
In 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 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.
Right-click the Models folder > Add > Class. Name the file Movie.cs .
C#
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models;
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.
The question mark after string indicates that the property is nullable. For more
information, see Nullable reference types.
Visual Studio
In Solution Explorer, right-click the Controllers folder and select Add > New
Scaffolded Item.
In the Add New Scaffolded Item dialog:
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.VisualStudio.Web.CodeGeneration.Design
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 .
PowerShell
Add-Migration InitialCreate
Update-Database
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.
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()'.
If you get an exception similar to the following, you may have missed the Update-
Database command in the migrations step:
Console
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 .
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)
{
}
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.
Visual Studio
C#
builder.Services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));
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.
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);
});
}
InitialCreate.Up creates the Movie table and configures Id as the primary key.
C#
MVC provides the ability to pass strongly typed model objects to a view. This strongly
typed approach enables compile time code checking. The scaffolding mechanism
passed a strongly typed model in the MoviesController class and views.
C#
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
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.
C#
If a movie is found, an instance of the Movie model is passed to the Details view:
C#
return View(movie);
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
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#
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
C#
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;
namespace MvcMovie.Models;
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.
}
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;
builder.Services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));
SeedData.Initialize(services);
}
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.
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;
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.
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
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>
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Tag Helpers are one of the most popular new features in ASP.NET Core. For more
information, see Additional resources.
Open the Movies controller and examine the two Edit action methods. The following
code shows the HTTP GET Edit method, which fetches the movie and populates the edit
form generated by the Edit.cshtml Razor file.
C#
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted
movie values:
C#
// 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.
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();
}
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
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
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 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.
C#
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
The following line in the Index action method creates a LINQ query to select the
movies:
C#
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));
}
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
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?}");
C#
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
C#
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#
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>
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.
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>
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.
CSHTML
<form asp-controller="Movies" asp-action="Index" method="get">
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models;
A list of movies.
A SelectList containing the list of genres. This allows the user to select a genre
from the list.
MovieGenre , which contains the selected genre.
SearchString , which contains the text users enter in the search text box.
C#
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string
searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
return View(movieGenreVM);
}
The following code is a LINQ query that retrieves all the genres from the database.
C#
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.
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>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies!)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-
id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-
id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
In the preceding code, the DisplayNameFor HTML Helper inspects the Title property
referenced in the lambda expression to determine the display name. Since the lambda
expression is inspected rather than evaluated, you don't receive an access violation
when model , model.Movies , or model.Movies[0] are null or empty. When the lambda
expression is evaluated (for example, @Html.DisplayFor(modelItem => item.Title) ), the
model's property values are evaluated. The ! after model.Movies is the null-forgiving
operator, which is used to declare that Movies isn't null.
Previous Next
Part 8, add a new field to an ASP.NET
Core MVC app
Article • 05/02/2023
By Rick Anderson
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models;
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.
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>
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:
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.)
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.
From the Tools menu, select NuGet Package Manager > Package Manager
Console.
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
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:
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.
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;
[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 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.
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>
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.
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#
[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 .
C#
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:
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.
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models;
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
C#
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The MVC scaffolding engine that created this action method adds a comment showing
an HTTP request that invokes the method. In this case it's a GET request with three URL
segments, the Movies controller, the Details method, and an id value. Recall these
segments are defined in Program.cs .
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
movie). If you didn't check for a null movie, the app would throw an exception.
C#
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
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
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.
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.
HomeController.cs :
C#
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:
C#
return View("Orders");
C#
return View(Orders);
C#
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
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:
ViewBag
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>
C#
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.
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.
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#
return 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
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#
return View();
}
}
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.
C#
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>
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.">
...
ViewData
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
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.
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:
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.
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.
HTML
HTML
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:
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>
XML
<PropertyGroup>
<StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>
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>
{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.
HTML
<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">
For information on Blazor CSS isolation, see ASP.NET Core Blazor CSS isolation.
Partial views in ASP.NET Core
Article • 05/17/2023
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.
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.
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.
C#
In ASP.NET Core 2.2 or later, a handler method can alternatively call the Partial method
to produce a PartialViewResult object:
C#
The Partial Tag Helper renders content asynchronously and uses an HTML-like syntax:
CSHTML
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
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
MVC
CSHTML
CSHTML
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")
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");
}
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.
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.
Razor Pages
MVC
1. /Areas/<Area-Name>/Views/<Controller-Name>
2. /Areas/<Area-Name>/Views/Shared
3. /Views/Shared
4. /Pages/Shared
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.
CSHTML
CSHTML
Razor Pages
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;
index++;
}
}
CSHTML
@model string
<div>
<h3>@Model</h3>
This partial view from /Pages/Shared/_AuthorPartialRP.cshtml.
</div>
CSHTML
@using PartialViewsSample.ViewModels
@model ArticleSection
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;
CSHTML
@model string
<div>
<h3>@Model</h3>
This partial view from /Views/Shared/_AuthorPartial.cshtml.
</div>
CSHTML
@using PartialViewsSample.ViewModels
@model ArticleSection
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
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
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.
A controller is an instantiable class, usually public, in which at least one of the following
conditions is true:
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.
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.
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.
Redirect
The Redirect result type differs from the HTTP Status Code type primarily in the
addition of a Location HTTP response header.
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.
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
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.
Handle errors
Response Caching
ASP.NET Core controllers use the Routing middleware to match the URLs of incoming
requests and map them to actions. Route templates:
This document:
C#
builder.Services.AddControllersWithViews();
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?}");
C#
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#
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.
C#
app.MapDefaultControllerRoute();
Replaces:
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
) Important
Conventional routing
Conventional routing is used with controllers and views. The default route:
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
id parameter to 17.
This mapping:
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:
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:
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?} :
Automatically assign an order value to their endpoints based on the order they are
invoked.
Enable Logging to see how the built-in routing implementations, such as Route, match
requests.
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:
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"
}.
blog route.
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
For example:
C#
[HttpPost]
public IActionResult Edit(int id, Product product)
{
return ControllerContext.MyDisplayRouteInfo(id, product.name);
}
}
Edit(int) is selected when the HTTP verb is anything else. Edit(int) is generally
called via GET .
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:
Are interchangeable.
Which one is used in documentation and code depends on the API being
described.
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#
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
C#
[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#
[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#
[Route("[controller]/[action]")]
public IActionResult About()
{
return ControllerContext.MyDisplayRouteInfo();
}
}
C#
[Route("[controller]/[action]")]
public class HomeController : Controller
{
[Route("~/")]
[Route("/Home")]
[Route("~/Home/Index")]
public IActionResult Index()
{
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.
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#
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.
[HttpGet]
[HttpPost]
[HttpPut]
[HttpDelete]
[HttpHead]
[HttpPatch]
Route templates
ASP.NET Core has the following route templates:
C#
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
[HttpGet] // GET /api/test2
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}
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
C#
C#
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 :
C#
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);
}
}
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);
}
}
See Routing for a full description of route templates and related options.
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:
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.
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);
}
}
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:
[Route("/")] No ""
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
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#
[Route("Home/About")]
[Route("Home/About/{id?}")]
public IActionResult About(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
C#
[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
WebMvcRouting.Controllers.HomeController.Index
WebMvcRouting.Controllers.MyDemoController.MyIndex
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 .
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#
[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
{
}
[HttpGet("{id}")] // /api/products11/edit/3
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
C#
using System.Text.RegularExpressions;
return Regex.Replace(value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}
C#
C#
using Microsoft.AspNetCore.Mvc.ApplicationModels;
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});
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();
2 Warning
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();
}
}
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.
C#
integers.
See Route Template Reference for a detailed description of route template syntax.
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.
C#
[MyApiController]
[ApiController]
public class MyTestApiController : ControllerBase
{
// GET /api/MyTestApi
[HttpGet]
public IActionResult Get()
{
return ControllerContext.MyDisplayRouteInfo();
}
}
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#
The following code prevents the namespace convention from being applied to
controllers that are attribute routed:
C#
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}");
}
C#
using My.Application.Controllers;
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(
new NamespaceRoutingConvention(typeof(HomeController).Namespace!));
});
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;
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}");
}
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.
C#
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
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 ;
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#
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
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:
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.
C#
[HttpGet("custom/url/to/destination")]
public IActionResult Destination()
{
return ControllerContext.MyDisplayRouteInfo();
}
}
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.
Warning: URL paths are hierarchical. In the preceding example, if the value { c = Cheryl
} is added:
You might expect to hit this problem with the default route
{controller}/{action}/{id?} . This problem is rare in practice because Url.Action
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:
Any additional route values that don't match route parameters are put in the query
string.
C#
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!);
}
C#
CSHTML
<h1>Test Links</h1>
<ul>
<li><a href="@Url.RouteUrl("Destination_Route")">Test
Destination_Route</a></li>
</ul>
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.
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 .
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 ,
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:
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#
builder.Services.AddControllersWithViews();
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();
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.
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;
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;
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;
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;
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);
}
}
}
C#
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
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.
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#
C#
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IDateTime, SystemDateTime>();
services.AddControllersWithViews();
}
The following code displays a greeting to the user based on the time of day:
C#
C#
public IActionResult About([FromServices] IDateTime dateTime)
{
return Content( $"Current server time: {dateTime.Now}");
}
C#
C#
services.AddControllersWithViews();
}
C#
The following code requests the IOptions<SampleWebSettings> settings from the service
container and uses them in the Index method:
C#
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.
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.
Configuration injection
The values in settings files, such as appsettings.json and
appsettings.Development.json , can be injected into a view. Consider the
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>
CSHTML
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
ViewData["Title"] = "Privacy MVC";
}
<h1>@ViewData["Title"]</h1>
<h2>
MyRoot:MyParent:MyChildName:
@Configuration["MyRoot:MyParent:MyChildName"]
</h2>
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;
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddTransient<IToDoItemRepository, ToDoItemRepository>();
builder.Services.AddTransient<StatisticsService>();
builder.Services.AddTransient<ProfileOptionsService>();
builder.Services.AddTransient<MyHtmlHelper>();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapRazorPages();
app.MapDefaultControllerRoute();
app.Run();
C#
using System.Linq;
using ViewInjectSample.Interfaces;
namespace ViewInjectSample.Model.Services
{
public class StatisticsService
{
private readonly IToDoItemRepository _toDoItemRepository;
The sample displays data from the model bound to the view and the service injected
into the view:
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;
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 />
C#
namespace ViewInjectSample.Model.Services;
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.
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.
The Home controller displays a list of brainstorming sessions and allows the creation of
new brainstorming sessions with a POST request:
C#
return View(model);
}
[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
});
}
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#
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.
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 .
C#
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:
C#
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Avoid returning business domain entities directly via API calls. Domain entities:
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);
}
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);
// 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);
}
return result;
}
The first test confirms that the controller returns an ActionResult but not a nonexistent
list of ideas for a nonexistent session id :
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:
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:
C#
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>>
CreateActionResult([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (session == null)
{
return NotFound(model.SessionId);
}
await _sessionRepository.UpdateAsync(session);
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);
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>
(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
[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);
// 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!
Using .NET for client-side web development offers the following advantages:
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.
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
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string? Title { get; set; }
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>
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:
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.
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.
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:
For more information on creating Blazor Hybrid apps with the preceding frameworks,
see the following articles:
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.
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
For Blazor Hybrid apps, we test on and support the latest platform Web View control
versions:
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.
Install the latest version of Visual Studio with the ASP.NET and web
development workload.
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.
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.
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:
For more information on Blazor project templates, see ASP.NET Core Blazor project
structure.
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
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.
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
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
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.
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.
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).
Downloading the .NET runtime, the app, and the app's dependencies.
Initialization of the runtime to run the app.
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).
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.
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 on Microsoft native client frameworks, see the following
resources:
Blazor Hybrid apps include .NET MAUI, WPF, and Windows Forms framework apps.
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.
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.
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.
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.
Use ASP.NET Core SignalR with Blazor (Blazor Server or Blazor WebAssembly)
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.
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.
.NET CLI
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
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 {
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 Shared/NavMenu.razor :
razor
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#
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.
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>
@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
@code {
private List<TodoItem> todos = new();
1. To get the title of the new todo item, add a newTodo string field at the top of the
@code block:
C#
Modify the text <input> element to bind newTodo with the @bind attribute:
razor
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>
@code {
private List<TodoItem> todos = new();
private string? newTodo;
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
razor
@page "/todo"
<PageTitle>Todo</PageTitle>
<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="todo.IsDone" />
<input @bind="todo.Title" />
</li>
}
</ul>
@code {
private List<TodoItem> todos = new();
private string? newTodo;
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:
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.
Prerequisites
Visual Studio
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.
Visual Studio
7 Note
Visual Studio 2022 or later and .NET Core SDK 6.0.0 or later are required.
Type BlazorServerSignalRApp in the Project name field. Confirm the Location entry
is correct or provide a location for the project. Select Next.
Select Create.
In the Manage NuGet Packages dialog, confirm that the Package source is set to
nuget.org .
If the License Acceptance dialog appears, select I Accept if you agree with the
license terms.
C#
using Microsoft.AspNetCore.SignalR;
namespace BlazorServerSignalRApp.Server.Hubs;
C#
using Microsoft.AspNetCore.ResponseCompression;
using BlazorServerSignalRApp.Server.Hubs;
C#
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
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");
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;
await hubConnection.StartAsync();
}
7 Note
Visual Studio
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:
Next steps
In this tutorial, you learned how to:
To learn more about building Blazor apps, see the Blazor documentation:
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.
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
C#
.NET MAUI configures the CurrentCulture and CurrentUICulture based on the device's
ambient information.
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.
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:
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:
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:
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.
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:
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.
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
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:
This tutorial shows you how to build and run a Windows Forms Blazor app. You learn
how to:
Prerequisites
Supported platforms (Windows Forms documentation)
Visual Studio 2022 with the .NET desktop development workload
In the Additional information dialog, select the framework version with the Framework
dropdown list. Select the Create button:
XML
<Project Sdk="Microsoft.NET.Sdk.Razor">
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
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>
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>
@code {
private int currentCount = 0;
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.
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#
7 Note
C#
using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;
namespace WinFormsBlazor;
This tutorial shows you how to build and run a WPF Blazor app. You learn how to:
Prerequisites
Supported platforms (WPF documentation)
Visual Studio 2022 with the .NET desktop development workload
In the Additional information dialog, select the framework version with the Framework
dropdown list. Select the Create button:
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
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 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>
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>
@code {
private int currentCount = 0;
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>
C#
using Microsoft.Extensions.DependencyInjection;
Inside the MainWindow constructor, after the InitializeComponent method call, add the
following code:
C#
7 Note
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;
This article explains how to manage request routing and navigation in Blazor Hybrid
apps.
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.
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:
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;
}
};
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.
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 .
diff
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;
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>
@code {
private async void OpenPage()
{
await App.Current.MainPage.Navigation.PushAsync(new
NavigationExample());
}
}
diff
- await Navigation.PopAsync();
+ await Navigation.PopModalAsync();
diff
- await App.Current.MainPage.Navigation.PushAsync(new
NavigationExample());
+ await App.Current.MainPage.Navigation.PushModalAsync(new
NavigationExample());
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:
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
Pages/StaticAssetExample.razor :
razor
@page "/static-asset-example"
@using System.IO
@using Microsoft.Extensions.Logging
@inject ILogger<StaticAssetExample> Logger
<p>@dataResourceText</p>
@code {
public string dataResourceText = "Loading resource ...";
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
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.
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.
2 Warning
StaticAssetExample.razor :
razor
@page "/static-asset-example"
@using System.Resources
<p>@dataResourceText</p>
@code {
public string dataResourceText = "Loading resource ...";
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
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.
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.
2 Warning
StaticAssetExample.razor :
razor
@page "/static-asset-example"
@using System.Resources
<h1>Static Asset Example</h1>
<p>@dataResourceText</p>
@code {
public string dataResourceText = "Loading resource ...";
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:
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
wwwroot/scripts.js :
JavaScript
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
<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>
<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;
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);
In WPF and Windows Forms apps, add the following ReadData method to the @code
block of the preceding component:
C#
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
Modify the module object reference in the StaticAssetExample2 component to use the
collocated JavaScript file path ( ./Pages/StaticAssetExample2.razor.js ):
C#
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.
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;
C#
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif
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:
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.
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.
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.
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:
.NET
ASP.NET Core
Blazor
.NET MAUI
Azure Container Apps
Orleans
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
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:
Placeholders:
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.
The following weather data example abstracts different weather forecast service
implementations:
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.
C#
namespace SharedLibrary.Data;
C#
using SharedLibrary.Data;
namespace SharedLibrary.Interfaces;
The _Imports.razor file in the RCL includes the following added namespaces:
razor
@using SharedLibrary.Data
@using SharedLibrary.Interfaces
C#
using System.Net.Http.Json;
using SharedLibrary.Data;
using SharedLibrary.Interfaces;
namespace {APP NAMESPACE}.Services;
In the preceding example, the {APP NAMESPACE} placeholder is the app's namespace.
C#
using SharedLibrary.Data;
using SharedLibrary.Interfaces;
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.
razor
@page "/fetchdata"
@inject IWeatherForecastService ForecastService
<PageTitle>Weather forecast</PageTitle>
<h1>Weather forecast</h1>
@code {
private WeatherForecast[]? forecasts;
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 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 KeypadViewModel()
{
// Command to add the key to the input string
AddCharCommand = new Command<string>((key) => InputString += key);
// 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
formatted = string.Format("({0}) {1}-{2}", str.Substring(0, 3),
str.Substring(3, 3), str.Substring(6));
return formatted;
}
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
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() }
};
}
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.
Pages/Keypad.razor :
razor
@page "/keypad"
<h1>Keypad</h1>
<table id="keypad">
<thead>
<tr>
<th colspan="2">@KeypadViewModel.DisplayText</th>
<th><button @onclick="DeleteChar">⇦</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; }
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>
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.
Integrating authentication must achieve the following goals for Razor components and
services:
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:
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 .
.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.
7 Note
ExternalAuthStateProvider.cs :
C#
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
C#
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
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.
authenticatedUser.Principal = user;
return host;
7 Note
ExternalAuthStateProvider.cs :
C#
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using 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>();
C#
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.
C#
authService.CurrentUser = authenticatedUser;
C#
7 Note
ExternalAuthStateProvider.cs :
C#
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
return loginTask;
return Task.FromResult(authenticatedUser);
}
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
@code
{
public async Task Login()
{
await ((ExternalAuthStateProvider)AuthenticationStateProvider)
.LogInAsync();
}
}
Shared/LogoutComponent.razor :
razor
@code
{
public async Task Logout()
{
await ((ExternalAuthStateProvider)AuthenticationStateProvider)
.Logout();
}
}
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.
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
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:
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:
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
2 Warning
The sandbox attribute is not supported in early browser versions. For more
information, see Can I use: sandbox .
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.
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
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:
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.
launchSettings.json file.
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.
wwwroot folder: The Web Root folder for the app containing the app's public static
assets.
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 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.
launchSettings.json file.
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.
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:
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:
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
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."
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>
@code {
private int currentCount = 0;
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:
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:
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
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:
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.
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.
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)]
In App.razor :
razor
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.
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.
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; }
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#
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:
Pages/User.razor :
razor
@page "/user/{Id:int}"
@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.
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; }
}
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");
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* .
Member Description
Member Description
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).
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.
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.
Location changes
For the LocationChanged event, LocationChangedEventArgs provides the following information about navigation
events:
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:
Pages/Navigate.razor :
razor
@page "/navigate"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<Navigate> Logger
@inject NavigationManager Navigation
@code {
private void NavigateToCounterComponent()
{
Navigation.NavigateTo("counter");
}
For more information on component disposal, see ASP.NET Core Razor component lifecycle.
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#
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:
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; }
Pages/Search.razor :
razor
@page "/search"
<h1>Search Example</h1>
<p>Filter: @Filter</p>
<p>Page: @Page</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; }
}
razor
...
Navigation.GetUriWithQueryParameter("{NAME}", {VALUE})
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.
razor
...
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
...
Navigation.GetUriWithQueryParameters("{URI}", {PARAMETERS})
bool
DateTime
decimal
double
float
Guid
int
long
string
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#
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
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/
C#
Navigation.GetUriWithQueryParameters(
new Dictionary<string, object?>
{
["name"] = null,
["age"] = (int?)25,
["eye color"] = "green"
})
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
C#
Navigation.GetUriWithQueryParameters(
new Dictionary<string, object?>
{
["full name"] = "Morena Baccarin",
["ping"] = new int?[] { 35, 16, null, 87, 240 }
})
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
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"));
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).
razor
<Navigating>
<p>Loading the requested page…</p>
</Navigating>
For an example that uses the Navigating property, see Lazy load assemblies in ASP.NET Core Blazor WebAssembly.
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.
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:
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.
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.
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
<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 };
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.
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;
return ValueTask.CompletedTask;
}
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.
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("/");
}
if (!isConfirmed)
{
context.PreventNavigation();
}
}
}
For additional example code, see the ConfigurableNavigationLock component in the BasicTestApp
(dotnet/aspnetcore reference source) .
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
HTML
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
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.
razor
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
7 Note
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.
wwwroot/appsettings.json :
JSON
{
"h1FontSize": "50px"
}
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"
}
C#
using Microsoft.Extensions.Configuration;
In Program.cs , modify the existing HttpClient service registration to use the client to
read the file:
C#
builder.Configuration.AddJsonStream(stream);
C#
using Microsoft.Extensions.Configuration.Memory;
In Program.cs :
C#
builder.Configuration.Add(memoryConfig);
Pages/MemoryConfig.razor :
razor
@page "/memory-config"
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
<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>
@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}"
}
}
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.
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"));
In Program.cs :
C#
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
This article explains how Blazor apps can inject services into components.
7 Note
Default services
The services shown in the following table are commonly used in Blazor apps.
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
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.
C#
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#
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#
await host.RunAsync();
C#
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
C#
builder.Services.AddSingleton<IDataAccess, DataAccess>();
First, factor common service registrations into a separate method. For example, create a
ConfigureCommonServices method in the Client project:
C#
C#
...
ConfigureCommonServices(builder.Services);
C#
...
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.
For more information, see Dependency injection into views in ASP.NET Core.
The following example shows how to use @inject. The service implementing
Services.IDataAccess is injected into the component's property DataRepository . Note
razor
@page "/customer-list"
@inject IDataAccess DataRepository
@code {
private IReadOnlyList<Customer>? customers;
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;
...
}
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!;
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;
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.
7 Note
Two versions of OwningComponentBase type are available and described in the next
two sections:
OwningComponentBase
OwningComponentBase<TService>
OwningComponentBase
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#
In Program.cs :
C#
builder.Services.AddScoped<ITimeTravel, TimeTravel>();
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; }
If you're placing this example into a test app, add the TimeTravel component to the
NavMenu component.
In Shared/NavMenu.razor :
razor
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:
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
new DT value.
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>
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>
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection
{
using BlazorWebAssemblyTransientDisposable;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
return builder;
}
return webAssemblyHost;
}
}
}
namespace BlazorWebAssemblyTransientDisposable
{
public class DetectIncorrectUsageOfTransientDisposablesServiceFactory
: IServiceProviderFactory<IServiceCollection>
{
public IServiceCollection CreateBuilder(IServiceCollection services)
=>
services;
collection.Add(CreatePatchedFactoryDescriptor(descriptor));
}
else
{
collection.Add(descriptor);
}
}
collection.AddScoped<ThrowOnTransientDisposable>();
return collection.BuildServiceProvider();
}
if (originalFactory is null)
{
throw new InvalidOperationException(
"originalFactory is null.");
}
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;
}
if (original.ImplementationType is null)
{
throw new InvalidOperationException(
"ImplementationType is null.");
}
return ActivatorUtilities.CreateInstance(sp,
original.ImplementationType);
},
ServiceLifetime.Transient);
return newDescriptor;
}
}
TransientDisposable.cs :
C#
public class TransientDisposable : IDisposable
{
public void Dispose() => throw new NotImplementedException();
}
Program.cs :
C#
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWebAssemblyTransientDisposable;
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped(sp =>
new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
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
7 Note
BaseAddressAuthorizationMessageHandler
AuthorizationMessageHandler
DetectIncorrectUsagesOfTransientDisposables.cs :
C#
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection
{
using BlazorServerTransientDisposable;
return builder;
}
}
}
namespace BlazorServerTransientDisposable
{
internal class ThrowOnTransientDisposableHandler : CircuitHandler
{
public ThrowOnTransientDisposableHandler(
ThrowOnTransientDisposable throwOnTransientDisposable)
{
throwOnTransientDisposable.ShouldThrow = true;
}
}
collection.Add(CreatePatchedFactoryDescriptor(descriptor));
}
else
{
collection.Add(descriptor);
}
}
collection.AddScoped<ThrowOnTransientDisposable>();
return collection.BuildServiceProvider();
}
if (originalFactory is null)
{
throw new InvalidOperationException(
"originalFactory is null.");
}
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;
}
if (original.ImplementationType is null)
{
throw new InvalidOperationException(
"ImplementationType is null.");
}
return ActivatorUtilities.CreateInstance(sp,
original.ImplementationType);
},
ServiceLifetime.Transient);
return newDescriptor;
}
}
TransitiveTransientDisposableDependency.cs :
C#
public TransientDependency(ITransitiveTransientDisposableDependency
transitiveTransientDisposableDependency)
{
this.transitiveTransientDisposableDependency =
transitiveTransientDisposableDependency;
}
}
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
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.
BlazorServiceAccessor.cs :
C#
The following class demonstrates the implementation for the base component.
CustomComponentBase.cs :
C#
using Microsoft.AspNetCore.Components;
[Inject]
private IServiceProvider Services { get; set; } = default!;
[Inject]
private BlazorServiceAccessor BlazorServiceAccessor { get; set; } =
default!;
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
});
Task IHandleAfterRender.OnAfterRenderAsync()
=> InvokeWithBlazorServiceContext(() =>
{
var firstRender = !hasCalledOnAfterRender;
hasCalledOnAfterRender |= true;
OnAfterRender(firstRender);
return OnAfterRenderAsync(firstRender);
});
throw;
}
StateHasChanged();
}
scope.
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
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.
JavaScript initializers
JavaScript (JS) initializers execute logic before and after a Blazor app loads. JS initializers
are useful in the following scenarios:
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).
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,
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
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.
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 following example loads script1.js before script2.js and script3.js before
script4.js :
JavaScript
additionalModule.js :
JavaScript
JavaScript
logMessage();
}
Import map
Import maps are supported by ASP.NET Core and Blazor.
CSHTML
<body>
...
<script src="_framework/blazor.{server|webassembly}.js"
autostart="false"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
Blazor.start();
});
</script>
</body>
CSHTML
<body>
...
<script src="_framework/blazor.{server|webassembly}.js"
autostart="false"></script>
<script>
Blazor.start().then(function () {
...
});
</script>
</body>
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:
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:
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.
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.
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 ).
HTML
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.
HTML
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.
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#
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.
C#
...
app.MapFallbackToFile("index.html", staticFileOptions);
For more information on CSPs, see Enforce a Content Security Policy for ASP.NET Core
Blazor.
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:
Using the preceding CSS variables, you can create custom progress indicators that
match the styling of your app.
startup.
totalResources is the total number of resources to load.
JavaScript
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.
.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;
}
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 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.
CSHTML
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.
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
...
<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.
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
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.
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>
In Program.cs :
C#
if (builder.HostEnvironment.Environment == "Custom")
{
...
};
In Program.cs :
C#
if (builder.HostEnvironment.IsStaging())
{
...
};
if (builder.HostEnvironment.IsEnvironment("Custom"))
{
...
};
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.
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.
Pages/Counter1.razor :
razor
@page "/counter-1"
@inject ILogger<Counter> logger
<h1>Counter</h1>
@code {
private int currentCount = 0;
currentCount++;
}
}
Pages/Counter2.razor :
razor
@page "/counter-2"
@inject ILoggerFactory LoggerFactory
<h1>Counter</h1>
@code {
private int currentCount = 0;
currentCount++;
}
}
C#
builder.Logging.SetMinimumLevel(LogLevel.Warning);
In Program.cs :
C#
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) .
The following example shows how to use log categories with the Counter component of
an app created from a Blazor project template.
C#
warn: CustomCategory[0]
Someone has clicked me!
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#
C#
info: BlazorSample.Pages.Counter[1000]
Someone has clicked me!
warn: BlazorSample.Pages.Counter[1001]
Someone has clicked me!
The following example shows how to use log message templates with the Counter
component of an app created from a Blazor project template.
C#
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.
C#
currentCount++;
try
{
if (currentCount == 3)
{
currentCount = 4;
throw new OperationCanceledException("Skip 3");
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Exception (currentCount: {Count})!",
currentCount);
}
warn: BlazorSample.Pages.Counter[0]
Exception (currentCount: 4)!
System.OperationCanceledException: Skip 3
at BlazorSample.Pages.Counter.IncrementCount() in
C:UsersAlabaDesktopBlazorSamplePagesCounter.razor:line 28
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#
C#
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.
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 .
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;
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 CustomLogger(
string name,
Func<CustomLoggerConfiguration> getCurrentConfig) =>
(this.name, this.getCurrentConfig) = (name, getCurrentConfig);
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);
}
CustomLoggerExtensions.cs :
C#
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration;
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();
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:
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
C#
builder.Logging.AddConfiguration(
builder.Configuration.GetSection("Logging"));
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.
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:
For the configureLogging log level value, pass the argument as either the string or
integer log level shown in the following table.
Trace trace 0
Debug debug 1
Information information 2
Warning warning 3
Error error 4
Critical critical 5
None none 6
HTML
HTML
For more information on Blazor startup ( Blazor.start() ), see ASP.NET Core Blazor
startup.
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.
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.
7 Note
7 Note
The following example is based on the Index component in the SignalR with
Blazor tutorial. Consult the tutorial for further details.
C#
await hubConnection.StartAsync();
}
7 Note
For more information on setting the app's environment for Blazor WebAssembly, see
ASP.NET Core Blazor environments.
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.
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.
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.
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>
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.
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.
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
In production, don't render framework exception messages or stack traces in the UI.
Rendering exception messages or stack traces could:
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:
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:
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.
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#
if (currentCount > 5)
{
throw new InvalidOperationException("Current count is too big!");
}
}
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;
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; }
7 Note
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>
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; }
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!
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
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.
‡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:
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.
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository
@code {
private ProductDetail? details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
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.
razor
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
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:
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 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.
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.
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.
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.
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.
In these scenarios, the Blazor WebAssembly thread or Blazor Server circuit fails and
usually attempts to:
To avoid infinite recursion patterns, ensure that recursive rendering code contains
suitable stopping conditions.
2 Warning
Use of manual render tree builder logic is considered an advanced and unsafe
scenario, not recommended for general component development.
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.
C#
if (!app.Environment.IsDevelopment())
{
app.UseResponseCompression();
app.UseExceptionHandler("/Error");
app.UseHsts();
}
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.
IncludeRequestCredentialsMessageHandler.cs :
C#
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Http;
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
return base.SendAsync(request, cancellationToken);
}
}
C#
...
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.
7 Note
The following error is thrown by an app that hasn't enabled sticky sessions in a
webfarm:
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
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;
});
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
For information on Blazor Server's memory model, see Host and deploy ASP.NET Core
Blazor Server.
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.
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.
Pages/_Host.cshtml :
CSHTML
<div id="components-reconnect-modal">
There was a problem with the connection!
</div>
7 Note
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.
components- A lost connection. The client is attempting to reconnect. Show the modal.
reconnect-
show
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:
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
wwwroot/css/site.css :
css
#components-reconnect-modal {
transition: visibility 0s linear 1000ms;
}
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
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):
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.
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
In the preceding markup, the {HOSTING MODEL} placeholder is either server for a Blazor
Server app or webassembly for a Blazor WebAssembly app.
C#
hubConnection.ServerTimeout = TimeSpan.FromSeconds(30);
hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(15);
hubConnection.KeepAliveInterval = TimeSpan.FromSeconds(15);
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:
To modify the connection events, register callbacks for the following connection
changes:
Pages/_Host.cshtml :
CSHTML
<body>
...
Pages/_Host.cshtml :
CSHTML
<body>
...
wwwroot/boot.js :
JavaScript
(() => {
const maximumRetryCount = 3;
const retryIntervalMilliseconds = 5000;
const reconnectModal = document.getElementById('reconnect-modal');
(async () => {
for (let i = 0; i < maximumRetryCount; i++) {
reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of
${maximumRetryCount}`;
try {
const result = await Blazor.reconnect();
if (!result) {
// The server was reached, but the connection was rejected;
reload the page.
location.reload();
return;
}
return {
cancel: () => {
isCanceled = true;
reconnectModal.style.display = 'none';
},
};
};
Blazor.start({
reconnectionHandler: {
onConnectionDown: () => currentReconnectionProcess ??=
startReconnectionProcess(),
onConnectionUp: () => {
currentReconnectionProcess?.cancel();
currentReconnectionProcess = null;
},
},
});
})();
For more information on Blazor startup, see ASP.NET Core Blazor startup.
Pages/_Host.cshtml :
CSHTML
<body>
...
For more information on Blazor startup, see ASP.NET Core Blazor startup.
JavaScript
window.addEventListener('pagehide', () => {
Blazor.disconnect();
});
For more information on Blazor startup, see ASP.NET Core Blazor startup.
TrackingCircuitHandler.cs :
C#
using Microsoft.AspNetCore.Components.Server.Circuits;
return Task.CompletedTask;
}
return Task.CompletedTask;
}
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>();
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.
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.
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.
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
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 examples, the {TFM} placeholder is the Target Framework Moniker
(TFM) (for example, net6.0 ).
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 ).
C#
using Microsoft.AspNetCore.StaticFiles;
...
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;
...
C#
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.
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 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.
✔️ ProductDetail.razor
❌ productDetail.razor
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:
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.
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
Pages/Counter.razor :
razor
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
@code {
private int currentCount = 0;
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>
Pages/CounterPartialClass.razor.cs :
C#
namespace BlazorSample.Pages;
@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.
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;
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;
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>
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#:
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.
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).
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
@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#
Shared/ParameterChild.razor :
razor
@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:
Pages/ParameterParent.razor :
razor
@page "/parameter-parent"
<ParameterChild />
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
<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>
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.
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.
Pages/ParameterParent2.razor :
razor
@page "/parameter-parent-2"
@code {
private string title = "From Parent field";
private PanelData panelData = new();
7 Note
razor
razor
Incorrect:
razor
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
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
@code {
private string? title;
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
The code in the preceding example generates a compiler error when the app is built:
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"
@code {
private PanelData panelData = new();
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.
C#
[Parameter]
public DateTime StartData { get; set; } = DateTime.Now;
C#
[Parameter]
[EditorRequired]
public string? Title { get; set; }
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.
Shared/RenderTupleChild.razor :
C#
@code {
[Parameter]
public Tuple<int, string, bool>? Data { get; set; }
}
Pages/RenderTupleParent.razor :
C#
@page "/render-tuple-parent"
@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) .
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; }
Shared/RenderFragmentChild.razor :
razor
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
) Important
Pages/RenderFragmentParent.razor :
razor
@page "/render-fragment-parent"
<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
<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
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
razor
<h1>Hello, world!</h1>
@RenderWelcomeInfo
@RenderWelcomeInfo
@code {
private RenderFragment RenderWelcomeInfo = __builder =>
{
<p>Welcome to your new app!</p>
};
}
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.
Pages/ReferenceParent1.razor :
razor
@page "/reference-parent-1"
@code {
private ReferenceChild? childComponent;
}
Pages/ReferenceParent2.razor :
razor
@page "/reference-parent-2"
@code {
private ReferenceChild? childComponent;
private Action? callChildMethod;
) Important
razor
@page "/"
@attribute [Authorize]
Pages/ConditionalAttribute.razor :
razor
@page "/conditional-attribute"
<label>
<input type="checkbox" checked="@IsCompleted" />
Is Completed?
</label>
@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
Pages/RazorTemplate.razor :
razor
@page "/razor-template"
@timeTemplate
@code {
private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.
</p>;
private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet:
@pet.Name</p>;
HTML
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
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.
HTML
Similarly, SVG images are supported in the CSS rules of a stylesheet file ( .css ):
css
.element-class {
background-image: url("image.svg");
}
razor
<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.";
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
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
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
Statically-rendered components can only be added to the app. They can't be removed
or updated afterwards.
razor
@typeparam TItem
razor
Shared/ListGenericTypeItems1.razor :
razor
@typeparam TExample
@code {
[Parameter]
public IEnumerable<TExample>? ExampleList{ get; set; }
}
The following GenericTypeExample1 component renders two ListGenericTypeItems1
components:
Pages/GenericTypeExample1.razor :
razor
@page "/generic-type-example-1"
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.
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.
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
@code {
[Parameter]
public IEnumerable<TExample>? ExampleList { get; set; }
}
Shared/ListDisplay2.razor :
razor
@typeparam TExample
@code {
[Parameter]
public IEnumerable<TExample>? ExampleList { get; set; }
}
7 Note
This section uses the two ListDisplay components in the Cascaded generic type
support section.
Shared/ListGenericTypeItems2.razor :
razor
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample
@ChildContent
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
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"
<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
Shared/ListDisplay4.razor :
razor
@typeparam TExample
@code {
[CascadingParameter]
protected IEnumerable<TExample>? ExampleList { get; set; }
}
Shared/ListGenericTypeItems3.razor :
razor
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample
@ChildContent
<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"
<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
razor
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
@ChildContent
<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"
Pages/GenericTypeExample5.razor :
razor
@page "/generic-type-example-5"
<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
Result
Wait
WaitAny
WaitAll
Sleep
GetResult
7 Note
TimerService.cs :
C#
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation($"elapsedCount: {elapsedCount}");
}
}
}
}
C#
C#
builder.Services.AddSingleton<NotifierService>();
builder.Services.AddSingleton<TimerService>();
C#
builder.Services.AddScoped<NotifierService>();
builder.Services.AddScoped<TimerService>();
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;
) Important
Consider the following example that demonstrates a collection mapping problem that's
solved by using @key.
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.
Shared/Details.razor :
razor
@code {
[Parameter]
public string? Data { get; set; }
}
Pages/PeopleExample.razor :
razor
@page "/people-example"
@using System.Timers
@implements IDisposable
@code {
private Timer timer = new Timer(3000);
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.
To modify the PeopleExample component to use the @key directive attribute with the
people collection, update the <Details> element to the following:
razor
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:
) Important
Keys are local to each container element or component. Keys aren't compared
globally across the document.
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:
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
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
razor
razor
<ol>
@foreach (var person in people)
{
<li>
<Details @key="person" Data="@person.Data" />
</li>
}
</ol>
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.
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
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.
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; }
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
Pages/ExpanderExample.razor :
razor
@page "/expander-example"
<Expander Expanded="true">
Expander 1 content
</Expander>
<button @onclick="StateHasChanged">
Call StateHasChanged
</button>
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.
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.
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
@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; }
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
Attribute splatting
In the following Splat component:
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";
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; }
}
razor
@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
Pages/AttributeOrderParent1.razor :
razor
@page "/attribute-order-parent-1"
HTML
In the following example, the order of extra and @attributes is reversed in the child
component's <div> :
Shared/AttributeOrderChild2.razor :
razor
@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
Pages/AttributeOrderParent2.razor :
razor
@page "/attribute-order-parent-2"
The <div> in the parent component's rendered webpage contains extra="10" when
passed through the additional attribute:
HTML
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
7 Note
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>
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):
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
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>
HTML
<body>
<div id="app">
<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>
<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>
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.
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.
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.
Shared/DoctorWhoLayout.razor :
razor
@inherits LayoutComponentBase
@layout ProductionsLayout
<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:
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>
<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>
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).
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"
<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>
App.razor :
razor
<PageTitle>Not found</PageTitle>
Additional resources
Control headers in C# code at startup
Blazor samples GitHub repository (dotnet/blazor-samples)
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.
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;
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.
Pages/ThemedCounter.razor :
razor
@page "/themed-counter"
@using BlazorSample.UIThemeClasses
<h1>Themed Counter</h1>
<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; }
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" };
razor
<CascadingValue Value="@parentCascadeParameter1" Name="CascadeParam1">
<CascadingValue Value="@ParentCascadeParameter2" Name="CascadeParam2">
...
</CascadingValue>
</CascadingValue>
@code {
private CascadingType? parentCascadeParameter1;
[Parameter]
public CascadingType? ParentCascadeParameter2 { get; set; }
}
razor
@code {
[CascadingParameter(Name = "CascadeParam1")]
protected CascadingType? ChildCascadeParameter1 { get; set; }
[CascadingParameter(Name = "CascadeParam2")]
protected CascadingType? ChildCascadeParameter2 { get; set; }
}
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.
UIInterfaces/ITab.cs :
C#
using Microsoft.AspNetCore.Components;
namespace BlazorSample.UIInterfaces;
7 Note
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
<CascadingValue Value="this">
<ul class="nav nav-tabs">
@ChildContent
</ul>
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
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; }
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>
@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.
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;
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; }
}
razor
@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
<h2>Elements</h2>
<h2>Components</h2>
@code {
private string text = "";
For more information on the InputText component, see ASP.NET Core Blazor forms and
input components.
Examples
Pages/BindGetSet.razor :
razor
@page "/bind-get-set"
@using Microsoft.AspNetCore.Components.Forms
<h2>Elements</h2>
<h2>Components</h2>
@code {
private string text = "";
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:
❌ 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;
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;
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.
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");
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.
Pages/BindMultipleInput.razor :
razor
@page "/bind-multiple-input"
<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" };
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.
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:
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
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 .
C#
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
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.
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
@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));
}
}
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>
@code {
private Random r = new();
private int year = 1979;
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
@code {
...
razor
Shared/PasswordEntry.razor :
razor
@code {
private bool showPassword;
private string? password;
[Parameter]
public string? Password { get; set; }
[Parameter]
public EventCallback<string> PasswordChanged { get; set; }
await PasswordChanged.InvokeAsync(password);
}
Pages/PasswordBinding.razor :
razor
@page "/password-binding"
<h1>Password Binding</h1>
<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
@code {
private bool showPassword;
private string? password;
private string? validationMessage;
[Parameter]
public string? Password { get; set; }
[Parameter]
public EventCallback<string> PasswordChanged { get; set; }
return Task.CompletedTask;
}
else
{
validationMessage = string.Empty;
return PasswordChanged.InvokeAsync(password);
}
}
razor
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>
<button @onclick="ChangeValue">Change from Parent</button>
</p>
@code {
private string parentMessage = "Initial value set in Parent";
Shared/NestedChild.razor :
razor
<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; }
Shared/NestedGrandchild.razor :
razor
<p>
<button @onclick="ChangeValue">Change from Grandchild</button>
</p>
</div>
@code {
[Parameter]
public string? GrandchildMessage { get; set; }
[Parameter]
public EventCallback<string> GrandchildMessageChanged { get; set; }
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.
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";
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}!!!";
}
}
Pages/EventHandlerExample3.razor :
razor
@page "/event-handler-example-3"
<p>@mousePointerMessage</p>
@code {
private string? mousePointerMessage;
Clipboard ClipboardEventArgs
Event Class Document Object Model (DOM) notes
Error ErrorEventArgs
Input ChangeEventArgs
Keyboard KeyboardEventArgs
Mouse MouseEventArgs
Mouse PointerEventArgs
pointer
Mouse WheelEventArgs
wheel
Progress ProgressEventArgs
7 Note
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.
JavaScript
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
C#
namespace BlazorSample.CustomEvents;
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 .
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.
JavaScript
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 preceding code tells the browser that when a native paste event occurs:
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;
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:
Pages/EventHandlerExample5.razor :
razor
@page "/event-handler-example-5"
<h1>@heading</h1>
<p>
<button @onclick="@(e => UpdateHeading(e, buttonNumber))">
Button #@i
</button>
</p>
}
@code {
private string heading = "Select a button to learn its position";
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:
Alternatively, use a foreach loop with Enumerable.Range, which doesn't suffer from the
preceding problem:
razor
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; }
}
Pages/Parent.razor :
razor
@page "/parent"
<h1>Parent-child example</h1>
<p>@message</p>
@code {
private string? message;
razor
<ChildComponent
OnClickCallback="@(async () => { await Task.Yield(); messageText =
"Blaze It!"; })" />
C#
await OnClickCallback.InvokeAsync(arg);
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;
razor
...
@code {
private bool shouldPreventDefault = true;
}
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:
Pages/EventHandlerExample7.razor :
razor
@page "/event-handler-example-7"
<label>
<input @bind="stopPropagation" type="checkbox" />
Stop Propagation
</label>
<p>
@message
</p>
@code {
private bool stopPropagation = false;
private string? message;
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;
This article explains the ASP.NET Core Razor component lifecycle and how to use
lifecycle events.
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.
7 Note
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:
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.
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 event handlers are provided in developer code, unhook them on disposal. For more
information, see the Component disposal with IDisposable IAsyncDisposable section.
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; }
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.
Pages/OnInit.razor :
razor
@page "/on-init"
<p>@message</p>
@code {
private string? message;
C#
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.
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.
For the following example component, navigate to the component's page at a URL:
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; }
Asynchronous work when applying parameters and property values must occur during
the OnParametersSetAsync lifecycle event:
C#
For more information on route parameters and constraints, see ASP.NET Core Blazor
routing and navigation.
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
@code {
private string message = "Initial assigned message.";
if (firstRender)
{
message = "Executed for the first render.";
}
else
{
message = "Executed after the first render.";
}
C#
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.
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.
razor
@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService
<h1>Weather forecast</h1>
@code {
private WeatherForecast[]? forecasts;
Handle errors
For information on handling errors during lifecycle method execution, see Handle errors
in ASP.NET Core Blazor apps.
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.
WeatherForecastService.cs :
C#
using Microsoft.Extensions.Caching.Memory;
await Task.Delay(TimeSpan.FromSeconds(10));
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.
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
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
@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
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
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).
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>
@code {
private string? data;
private ElementReference divElement;
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
Developer code must ensure that IAsyncDisposable implementations don't take a long
time to complete.
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.
At a minimum, always dispose objects created on the .NET side to avoid leaking .NET
managed memory.
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} .
Synchronous IDisposable
For synchronous disposal tasks, use IDisposable.Dispose.
The following component:
razor
@implements IDisposable
...
@code {
...
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
@code {
private int currentCount = 0;
private Timer timer = new(1000);
7 Note
Pages/CounterWithTimerDisposal2.razor :
razor
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable
@code {
private int currentCount = 0;
private Timer? timer;
Asynchronous IAsyncDisposable
For asynchronous disposal tasks, use IAsyncDisposable.DisposeAsync.
razor
@implements IAsyncDisposable
...
@code {
...
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
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:
razor
@implements IDisposable
<EditForm EditContext="@editContext">
...
<button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>
@code {
...
editContext.OnFieldChanged += fieldChanged;
}
razor
@implements IDisposable
<EditForm EditContext="@editContext">
...
<button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>
@code {
...
C#
C#
[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }
messageStore = new(CurrentEditContext);
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.
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.
Pages/BackgroundWork.razor :
razor
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger
@code {
private Resource resource = new();
private CancellationTokenSource cts = new();
cts.Token.ThrowIfCancellationRequested();
resource.BackgroundResourceMethod(Logger);
}
if (disposed)
{
logger.LogInformation("BackgroundResourceMethod: Disposed");
throw new ObjectDisposedException(nameof(Resource));
}
This article explains how to use component virtualization in ASP.NET Core Blazor apps.
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:
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:
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>
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.
razor
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.
C#
razor
@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));
}
}
C#
...
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:
razor
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>
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
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.
For example, you can use a tabindex attribute on the scroll container:
razor
To learn more about the meaning of tabindex value -1 , 0 , or other values, see tabindex
(MDN documentation) .
razor
HTML
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.
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 ).
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 ).
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>
Pages/VirtualizedTable.razor :
razor
@page "/virtualized-table"
<HeadContent>
<style>
html, body { overflow-y: scroll }
</style>
</HeadContent>
<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:
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.
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
Pages/ControlRender.razor :
razor
@page "/control-render"
<label>
<input type="checkbox" @bind="shouldRender" />
Should Render?
</label>
<p>
<button @onclick="IncrementCount">Click me</button>
</p>
@code {
private int currentCount = 0;
private bool shouldRender = true;
protected override bool ShouldRender()
{
return shouldRender;
}
However, it might make sense to call StateHasChanged in the cases described in the
following sections of this article:
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;
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.
Pages/CounterState2.razor :
razor
@page "/counter-state-2"
@using System.Timers
@implements IDisposable
<p>
Current count: @currentCount
</p>
@code {
private int currentCount = 0;
private Timer timer = new(1000);
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 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
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.
7 Note
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
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>
@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" }
};
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" }
};
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" }
};
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" }
};
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:
file.
Pages/Example.razor :
razor
@page "/example"
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.
HTML
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
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;
}
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
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>
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
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="..." /> .
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>
XML
<PropertyGroup>
<StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>
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>
XML
<ScopedCssEnabled>false</ScopedCssEnabled>
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.
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
component.
razor
@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.
razor
<button @onclick="Refresh">Refresh</button>
@code {
private DynamicComponent? dc;
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.
SpaceX® Shared/SpaceX.razor
ULA® Shared/UnitedLaunchAlliance.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
<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"
<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>
@code {
private Type? selectedType;
private void OnDropdownChange(ChangeEventArgs e)
{
selectedType = e.Value?.ToString()?.Length > 0 ?
Type.GetType($"BlazorSample.Shared.{e.Value}") : null;
}
}
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.
ComponentMetadata.cs :
C#
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; }
}
UnitedLaunchAlliance ( Shared/UnitedLaunchAlliance.razor )
VirginGalactic ( Shared/VirginGalactic.razor )
Pages/DynamicComponentExample2.razor :
razor
@page "/dynamiccomponent-example-2"
<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>
@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;
ComponentMetadata.cs :
C#
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
<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:
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"
<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;
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)
}
}
}
}
};
}
}
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
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 .
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);
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
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}
Add the following <base> tag and HeadOutlet component Tag Helper to the
<head> element:
CSHTML
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
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.
razor
<h1>Counter</h1>
@code {
private int currentCount = 0;
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";
}
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
For more information, see the Render components from a page or view section.
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
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.
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");
Pages/RoutableCounter.razor :
razor
@page "/routable-counter"
<PageTitle>Routable Counter</PageTitle>
<h1>Routable Counter</h1>
@code {
private int currentCount = 0;
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
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.
For more information on the Component Tag Helper, including passing parameters
and RenderMode configuration, see Component Tag Helper in ASP.NET Core.
C#
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>
@code {
private int currentCount = 0;
To render a component from a page or view, use the Component Tag Helper.
CSHTML
<h1>Razor Page</h1>
@functions {
[BindProperty(SupportsGet=true)]
public int InitialValue { get; set; }
}
CSHTML
<h1>Razor Page</h1>
<form>
<input type="number" asp-for="InitialValue" />
<button type="submit">Set initial value</button>
</form>
@functions {
[BindProperty(SupportsGet=true)]
public int InitialValue { get; set; }
}
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:
CSHTML
The _ViewImports.cshtml file is located in the Pages folder of a Razor Pages app or the
Views folder of an MVC app.
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>
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>
@code {
private WeatherForecast[] forecasts = Array.Empty<WeatherForecast>();
private PersistingComponentStateSubscription persistingSubscription;
if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>(
"fetchdata", out var restored))
{
forecasts =
await
WeatherForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now))
;
}
else
{
forecasts = restored!;
}
}
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.
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.
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.
Components can be shared in a Razor class library (RCL) across projects. Include
components and static assets in an app from:
Just as components are regular .NET types, components provided by an RCL are normal
.NET assemblies.
Create an RCL
Visual Studio
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
XML
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
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.
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.
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"
<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
<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';
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.
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.
css
.extra-style {
border: 2px dashed blue;
padding: 1em;
margin: 1em 0;
background-image: url('extra-background.png');
}
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
<ExtraStyles />
Link to the library's stylesheet in the app's <head> markup (location of <head> content).
HTML
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.
razor
<h3>ComponentLibrary.JeepYJ</h3>
<p>
<img alt="Jeep YJ®" 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®" 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>
For more information, see Reusable Razor UI in class libraries with ASP.NET Core.
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:
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
Blazor example:
Pages/Index.razor.js :
JavaScript
razor
C#
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()
{
...
}
.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.
The example in this section renders the following Razor component into a page via JS.
Shared/Quote.razor :
razor
@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;
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
The following example demonstrates the dynamic registration of the preceding Quote
component with " quote " as the identifier.
C#
builder.Services.AddServerSideBlazor(options =>
{
options.RootComponents.RegisterForJavaScript<Quote>(identifier:
"quote",
javaScriptInitializer: "initializeComponent");
});
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
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.
wwwroot/js/scripts.js :
JavaScript
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
<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
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
JavaScript
...
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.
JavaScript
7 Note
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.
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 .
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.
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.
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.
Pages/Counter.razor :
razor
@page "/counter"
<h1>Counter</h1>
@code {
private int currentCount = 0;
[Parameter]
public int IncrementAmount { get; set; } = 1;
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
You can update parameter values at any time using either attribute or property syntax.
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.
2 Warning
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.
This article describes how to use Blazor's globalization and localization features based
on:
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.
date
number
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>
In runtimeconfig.json :
JSON
{
"runtimeOptions": {
"configProperties": {
"System.Globalization.Invariant": true
}
}
}
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
<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>
<p>
The following <code><input></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>
<p>
The following <code><input></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;
}
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();
C#
app.UseRequestLocalization(new RequestLocalizationOptions()
.AddSupportedCultures(new[] { "en-US", "es-CL" })
.AddSupportedUICultures(new[] { "en-US", "es-CL" }));
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 ):
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 ):
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 .
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();
C#
app.UseRequestLocalization(localizationOptions);
The following example shows how to set the current culture in a cookie that can be read
by the Localization Middleware.
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)));
+ }
builder.Services.AddControllers();
C#
app.MapControllers();
The following example shows the call to UseEndpoints after the line is added:
C#
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
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
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"),
};
Navigation.NavigateTo(
$"Culture/Set?culture={cultureEscaped}&redirectUri=
{uriEscaped}",
forceLoad: true);
}
}
}
}
razor
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 .
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();
C#
app.UseRequestLocalization(localizationOptions);
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:
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
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
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.
razor
@using Microsoft.Extensions.Localization
Pages/CultureExample2.razor :
razor
@page "/culture-example-2"
@using System.Globalization
@inject IStringLocalizer<CultureExample2> Loc
<p>
<b>CurrentCulture</b>: @CultureInfo.CurrentCulture
</p>
<h2>Greeting</h2>
<p>
@Loc["Greeting"]
</p>
<p>
@greeting
</p>
@code {
private string? greeting;
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.
Localization/SharedResource.cs :
C#
namespace BlazorSample.Localization;
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
razor
@using Localization
@inject IStringLocalizer<SharedResource> Loc
razor
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:
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
ExampleModel.cs :
C#
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
<button type="submit">Submit</button>
</EditForm>
@code {
private ExampleModel exampleModel = new();
To demonstrate how the preceding EditForm component works with data annotations
validation:
ExampleModel.cs :
C#
using System.ComponentModel.DataAnnotations;
Pages/FormExample1.razor :
razor
@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger
<button type="submit">Submit</button>
</EditForm>
@code {
private ExampleModel exampleModel = new();
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
@code {
private ExampleModel exampleModel = new() { ... };
}
Assignment to EditForm.EditContext:
razor
@code {
private ExampleModel exampleModel = new() { ... };
private EditContext? editContext;
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.
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.
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:
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:
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
Starship.cs :
C#
using System.ComponentModel.DataAnnotations;
[Required]
public string? Classification { 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; }
}
Pages/FormExample2.razor :
razor
@page "/form-example-2"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample2> Logger
<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 };
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
7 Note
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
<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;
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
<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();
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.
<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:
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:
InputDate<TValue>.ParsingErrorMessage
InputNumber<TValue>.ParsingErrorMessage
css
razor
<label>
Production Date:
<InputDate @bind-Value="starship.ProductionDate"
DisplayName="Production Date" />
</label>
razor
<label>
Production Date:
<InputDate @bind-Value="starship.ProductionDate"
DisplayName="Production Date"
ParsingErrorMessage="The {0} field has an incorrect date
value." />
</label>
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.
Pages/FormExample4.razor :
razor
@page "/form-example-4"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample4> Logger
<h2>Ship Holodecks</h2>
<label>
Type 2:
<InputCheckbox @bind-Value="holodeck.Type2" />
</label>
<button type="submit">Update</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 EditContext? editContext;
private Holodeck holodeck = new();
private ValidationMessageStore? messageStore;
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) .
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.
7 Note
C#
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace BlazorSample;
[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }
CurrentEditContext.NotifyValidationStateChanged();
}
}
) Important
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
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.
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
<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 };
if (errors.Any())
{
customValidation?.DisplayErrors(errors);
}
else
{
Logger.LogInformation("HandleValidSubmit called: Processing the
form");
7 Note
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.
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.
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;
}
[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");
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.
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
Add the Microsoft.AspNetCore.Mvc namespace to the top of the Program.cs file in the
Server app:
C#
using Microsoft.AspNetCore.Mvc;
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
<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>
<p style="@messageStyles">
@message
</p>
<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 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);
7 Note
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:
ExampleModel.cs :
C#
using System.ComponentModel.DataAnnotations;
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
<button type="submit">Submit</button>
</EditForm>
<p>
CurrentValue: @exampleModel.Name
</p>
@code {
private ExampleModel exampleModel = new();
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 }
}
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:
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> </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
razor
<ValidationSummary />
Output validation messages for a specific model with the Model parameter:
razor
razor
css
.validation-message {
color: red;
}
CustomValidator.cs :
C#
using System;
using System.ComponentModel.DataAnnotations;
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#
C#
builder.Services.AddTransient<SaladChef>();
C#
using System.ComponentModel.DataAnnotations;
if (saladChef.ThingsYouCanPutInASalad.Contains(value?.ToString()))
{
return ValidationResult.Success;
}
( SaladIngredient ).
Pages/ValidationWithDI.razor :
razor
@page "/validation-with-di"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms
<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; }
}
ExampleModel.cs :
C#
using System.ComponentModel.DataAnnotations;
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.
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;
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
<button type="submit">Submit</button>
</EditForm>
@code {
private ExampleModel exampleModel = new();
private EditContext? editContext;
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;
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#
razor
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:
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;
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
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
Starship.cs :
C#
using System;
using System.ComponentModel.DataAnnotations;
[ValidateComplexType]
public ShipDescription ShipDescription { get; set; } = new();
...
}
ShipDescription.cs :
C#
using System;
using System.ComponentModel.DataAnnotations;
[Required]
[StringLength(240, ErrorMessage = "Description too long (240 char).")]
public string? LongDescription { get; set; }
}
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
Pages/FormExample9.razor :
razor
@page "/form-example-9"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample9> Logger
<p>
<label>
Identifier:
<InputText @bind-Value="starship.Identifier" />
</label>
</p>
@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;
If a form isn't preloaded with valid values and you wish to disable the Submit button on
form load, set formInvalid to true .
razor
...
@code {
private string displaySummary = "display:none";
...
private void HandleValidSubmit()
{
displaySummary = "display:block";
}
}
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.
JavaScript
For information on where to place JS in a Blazor app, see ASP.NET Core Blazor JavaScript
interoperability (JS interop).
ExampleModel2.cs :
C#
Pages/FormExample10.razor :
razor
@page "/form-example-10"
@inject IJSRuntime JS
@inject ILogger<FormExample10> Logger
<button type="submit">Submit</button>
</EditForm>
<p>
TextAreaValue length: @exampleModel.TextAreaValue1.Length
</p>
@code {
private ExampleModel2 exampleModel = new();
private ElementReference largeTextArea;
throw;
}
}
}
Troubleshoot
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#
Connection disconnected
Error: Connection disconnected with error 'Error: Server returned an error on close:
Connection closed with an error.'.
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
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
Rendered HTML:
HTML
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));
C#
✔️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());
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
7 Note
For testing file upload components, you can create test files of any size with
PowerShell:
PowerShell
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;
var trustedFileNameForFileStorage =
Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
isLoading = false;
}
}
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
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.
UploadResult.cs :
C#
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:
2 Warning
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;
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,
};
shouldRender = true;
}
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)",
fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
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.
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;
[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();
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);
}
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;
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
File streams
In Blazor Server, file data is streamed over the SignalR connection into .NET code on the
server as the file is read.
razor
razor
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:
JavaScript
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.
Pages/FileUpload4.razor :
razor
@page "/file-upload-4"
@inject IJSRuntime JS
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
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 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.
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.
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).
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.
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.
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).
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
<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;
}
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#
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:
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!"
HTML
<script>
window.triggerFileDownload = (fileName, url) => {
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
}
</script>
7 Note
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
<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);
}
}
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:
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).
7 Note
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) .
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 .
For more information, see Call JavaScript functions from .NET methods 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:
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:
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
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.
HTML
<body>
...
<script src="_framework/blazor.{server|webassembly}.js"></script>
<script>
window.jsMethod = (methodParameter) => {
...
};
</script>
</body>
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 .
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:
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
Blazor example:
Pages/Index.razor.js :
JavaScript
razor
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
C#
For more information on RCLs, see Consume ASP.NET Core Razor components from a
Razor class library (RCL).
HTML
<body>
...
<script src="_framework/blazor.{server|webassembly}.js"></script>
<script src="{SCRIPT PATH AND FILE NAME (.js)}"></script>
</body>
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>
HTML
<script src="./_content/ComponentLibrary/scripts.js"></script>
For more information, see Consume ASP.NET Core Razor components from a Razor class
library (RCL).
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");
For browser compatibility, see Can I use: JavaScript modules: dynamic import .
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.
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
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.
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
Pages/CallJsExample1.razor :
razor
@page "/call-js-example-1"
@inject IJSRuntime JS
<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,
};
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.
HTML
<script>
window.displayTickerAlert1 = (symbol, price) => {
alert(`${symbol}: $${price}!`);
};
</script>
7 Note
component.
Pages/CallJsExample2.razor :
razor
@page "/call-js-example-2"
@inject IJSRuntime JS
<p>
<button @onclick="SetStock">Set Stock</button>
</p>
@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
C#
using Microsoft.JSInterop;
Pages/CallJsExample3.razor :
razor
@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS
<p>
<button @onclick="SetStock">Set Stock</button>
</p>
@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
private JsInteropClasses1? jsClass;
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).
Pages/CallJsExample4.razor :
razor
@page "/call-js-example-4"
@inject IJSRuntime JS
<p>
<button @onclick="SetStock">Set Stock</button>
</p>
@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
private string? result;
C#
using Microsoft.JSInterop;
TickerChanged calls the handleTickerChanged2 method and displays the returned string
Pages/CallJsExample5.razor :
razor
@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS
<p>
<button @onclick="SetStock">Set Stock</button>
</p>
@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
private JsInteropClasses2? jsClass;
private string? result;
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
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
@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
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
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).
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>
@code {
private string? data;
private ElementReference divElement;
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
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");
}
}
razor
...
@inject IJSRuntime JS
@implements IAsyncDisposable
...
@code {
...
private IJSInProcessObjectReference? module;
...
JavaScript location
Load JavaScript (JS) code using any of approaches described by the JavaScript (JS)
interoperability (interop) overview article:
2 Warning
Don't place a <script> tag in a component file ( .razor ) because the <script> tag
can't be updated dynamically.
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
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
Pages/CallJsExample6.razor :
razor
@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS
<p>
<button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>
<p>
@result
</p>
@code {
private IJSObjectReference? module;
private string? result;
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.
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#
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) .
razor
@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).
The JS function clickElement creates a click event on the passed HTML element
( element ):
JavaScript
window.interopFunctions = {
clickElement : function (element) {
element.click();
}
}
razor
@inject IJSRuntime JS
<button @onclick="TriggerClick">
Trigger click event on <code>Example Button</code>
</button>
@code {
private ElementReference exampleButton;
To use an extension method, create a static extension method that receives the
IJSRuntime instance:
C#
razor
@inject IJSRuntime JS
@using JsInteropClasses
<button @onclick="TriggerClick">
Trigger click event on <code>Example Button</code>
</button>
@code {
private ElementReference exampleButton;
) Important
When working with generic types and returning a value, use ValueTask<TResult>:
C#
razor
@inject IJSRuntime JS
@using JsInteropClasses
<p>
returnValue: @returnValue
</p>
@code {
private ElementReference username;
private string? returnValue;
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.
HTML
<style>
.red { color: red }
</style>
HTML
<script>
function setElementClass(element, className) {
var myElement = element;
myElement.classList.add(className);
}
</script>
7 Note
razor
@page "/call-js-example-7"
Pages/CallJsExample7.razor.cs :
C#
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
namespace BlazorSample.Pages;
subscriptions.Clear();
}
subscriptions.Add(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.
razor
<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;
[Parameter]
public IObservable<ElementReference>? Parent { get; set; }
subscription?.Dispose();
subscription =
Parent is not null ? Parent.Subscribe(this) : null;
}
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.
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.
C#
builder.Services.AddServerSideBlazor(
options => options.JSInteropDefaultCallTimeout = {TIMEOUT});
Set a per-invocation timeout in component code. The specified timeout overrides the
global timeout set by JSInteropDefaultCallTimeout:
C#
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.
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
<div @ref="unmanagedElement"></div>
@code {
private ElementReference unmanagedElement;
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';
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
@code
{
private ElementReference mapElement;
private IJSObjectReference? mapModule;
private IJSObjectReference? mapInstance;
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.
HTML
<script>
window.receiveByteArray = (bytes) => {
let utf8decoder = new TextDecoder();
let str = utf8decoder.decode(bytes);
return str;
};
</script>
7 Note
Pages/CallJsExample9.razor :
razor
@page "/call-js-example-9"
@inject IJSRuntime JS
<p>
<button @onclick="SendByteArray">Send Bytes</button>
</p>
<p>
@result
</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/nm0821612/">Jewel Staite on IMDB</a>
</p>
@code {
private string? result;
For information on using a byte array when calling .NET from JavaScript, see Call .NET
methods from JavaScript functions in ASP.NET Core Blazor.
leaveOpen : Determines if the stream is left open after transmission. If a value isn't
provided, leaveOpen defaults to false .
Using an ArrayBuffer :
JavaScript
Using a ReadableStream :
JavaScript
In C# code:
C#
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.
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:
Pages/CallJsExample11.razor :
C#
@page "/call-js-example-11"
@inject IJSRuntime JS
<p>
<button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>
<p>
@result
</p>
<p>
@errorMessage
</p>
@code {
private string? errorMessage;
private string? result;
HTML
<script>
class Helpers {
static #controller = new AbortController();
static stopFn() {
this.#controller.abort();
console.log('longRunningFn aborted!');
}
}
window.Helpers = Helpers;
</script>
7 Note
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
<p>
<button @onclick="StartTask">Start Task</button>
<button @onclick="CancelTask">Cancel Task</button>
</p>
@code {
private CancellationTokenSource? cts;
Console
longRunningFn: 1
longRunningFn: 2
longRunningFn: 3
longRunningFn aborted!
For more information, see JavaScript JSImport/JSExport interop with ASP.NET Core
Blazor WebAssembly.
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.
At a minimum, always dispose objects created on the .NET side to avoid leaking .NET
managed memory.
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} .
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:
C#
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
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.
Pass in the name of the assembly containing the method, the identifier of the static .NET
method, and any arguments.
JavaScript
) Important
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.
Pages/CallDotNetExample1.razor :
razor
@page "/call-dotnet-example-1"
<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.
HTML
<script>
window.returnArrayAsync = () => {
DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
.then(data => {
console.log(data);
});
};
</script>
7 Note
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.
HTML
<script>
window.returnArrayAsync = (startPosition) => {
DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync',
startPosition)
.then(data => {
console.log(data);
});
};
</script>
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")]
DotNet.invokeMethodAsync('BlazorSample', 'DifferentMethodName');
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.
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.
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
JavaScript
DotNet.disposeJSObjectReference(jsObjectReference);
In the preceding example, the {ASSEMBLY NAME} placeholder is the app's namespace.
JavaScript
7 Note
) Important
The following sections of this article demonstrate various approaches for invoking an
instance .NET method:
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;
[DynamicDependency(nameof(ExampleJSInvokableMethod))]
public ExampleClass()
{
}
[JSInvokable]
public string ExampleJSInvokableMethod()
{
...
}
}
For more information, see Prepare .NET libraries for trimming: DynamicDependency.
HTML
<script>
window.sayHello1 = (dotNetHelper) => {
return dotNetHelper.invokeMethodAsync('GetHelloMessage');
};
</script>
7 Note
In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.
Pages/CallDotNetExample2.razor :
razor
@page "/call-dotnet-example-2"
@implements IDisposable
@inject IJSRuntime JS
<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;
[JSInvokable]
public string GetHelloMessage() => $"Hello, {name}!";
In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.
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.
Pages/CallDotNetExample3.razor :
razor
@page "/call-dotnet-example-3"
@implements IDisposable
@inject IJSRuntime JS
<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;
[JSInvokable]
public string GetHelloMessage(string passedName) => $"Hello,
{passedName}!";
In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.
Pages/CallDotNetExampleOneHelper.razor :
C#
@page "/call-dotnet-example-one-helper"
@implements IDisposable
@inject IJSRuntime JS
<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;
[JSInvokable]
public string GetHelloMessage() => $"Hello, {name}!";
[JSInvokable]
public string GetWelcomeMessage() => $"Welcome, {name}!";
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;
}
window.GreetingHelpers = GreetingHelpers;
</script>
7 Note
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.
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 .
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.
GenericType.cs :
C#
using Microsoft.JSInterop;
[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}");
}
}
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
HTML
<script>
const randomInt = () => Math.floor(Math.random() * 99999);
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
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;
await JS.InvokeVoidAsync(
"invokeMethodsAsync", syncInterop, objRef1, objRef2);
}
The following demonstrates typical output of the preceding example when the Invoke
Interop button is selected in a Blazor WebAssembly app:
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.
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.
HTML
<script>
window.sayHello1 = (dotNetHelper) => {
return dotNetHelper.invokeMethodAsync('GetHelloMessage');
};
</script>
7 Note
In the preceding example, the variable name dotNetHelper is arbitrary and can be
changed to any preferred name.
C#
using Microsoft.JSInterop;
[JSInvokable]
public string GetHelloMessage() => $"Hello, {Name}!";
}
JsInteropClasses3.cs :
C#
using Microsoft.JSInterop;
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
<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;
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
<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;
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.
JavaScript
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.
MessageUpdateInvokeHelper.cs :
C#
using Microsoft.JSInterop;
[JSInvokable]
public void UpdateMessageCaller()
{
action.Invoke();
}
}
HTML
<script>
window.updateMessageCaller = (dotNetHelper) => {
dotNetHelper.invokeMethodAsync('UpdateMessageCaller');
dotNetHelper.dispose();
}
</script>
7 Note
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.
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;
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"
<ul>
<ListItem1 />
<ListItem1 />
<ListItem1 />
<ListItem1 />
</ul>
The following image shows the rendered CallDotNetExample6 parent component after
the second InteropCall button is selected:
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 ).
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
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> ).
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.";
[JSInvokable]
public void UpdateMessage()
{
message = "UpdateMessage Called!";
display = "none";
StateHasChanged();
}
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"
<ul>
<ListItem2 />
<ListItem2 />
<ListItem2 />
<ListItem2 />
</ul>
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 .
JavaScript location
Load JavaScript (JS) code using any of approaches described by the JS interop overview
article:
2 Warning
Don't place a <script> tag in a component file ( .razor ) because the <script> tag
can't be updated dynamically.
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");
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
Pages/CallDotNetExample8.razor :
razor
@page "/call-dotnet-example-8"
@using System.Text
<p>
<button onclick="sendByteArray()">Send Bytes</button>
</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/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.
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);
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.
For more information, see JavaScript JSImport/JSExport interop with ASP.NET Core
Blazor WebAssembly.
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.
At a minimum, always dispose objects created on the .NET side to avoid leaking .NET
managed memory.
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} .
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:
C#
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.
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
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:
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.
7 Note
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.
XML
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
2 Warning
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>
@code {
private string? message;
message = GetWelcomeMessage();
}
}
7 Note
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.
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
Func<TResult> Function
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 .
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")]
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).
Pages/CallJavaScript1.razor.js :
JavaScript
The following CallDotNet1 component calls JS that directly interacts with the DOM to
render the welcome message string:
) 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");
}
To export a .NET method so that it can be called from JS, use the [JSExport] attribute.
.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.
Pages/CallDotNet1.razor.js :
JavaScript
document.getElementById("result").innerText =
exports.BlazorSample.Pages.CallDotNet1.GetMessageFromDotnet();
}
7 Note
Interop class ( Interop.cs ): Sets up import and export JS interop with the
wwwroot/js/interop.js .
CallJavaScript2 component ( Pages/CallJavaScript2.razor ): Calls
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
document.getElementById("result").innerText =
exports.BlazorSample.JavaScriptInterop.Interop.GetMessageFromDotnet();
}
C#
using System.Runtime.InteropServices.JavaScript;
C#
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;
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:
PutAsJsonAsync , DeleteAsync ).
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.
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
@code {
private IEnumerable<GitHubBranch>? branches = Array.Empty<GitHubBranch>
();
private bool getBranchesError;
private bool shouldRender;
if (response.IsSuccessStatusCode)
{
using var responseStream = await
response.Content.ReadAsStreamAsync();
branches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(responseStream);
}
else
{
getBranchesError = true;
}
shouldRender = true;
}
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.
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:
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.
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.
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"
@code {
private string? imageSource;
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
razor
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:
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
7 Note
Pages/ShowImage2.razor :
razor
@page "/show-image-2"
@inject HttpClient Http
@inject IJSRuntime JS
<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");
}
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.
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.
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.
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.
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 .
7 Note
Pages/ClaimsPrincipalData.razor :
razor
@page "/claims-principle-data"
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider
<h1>ClaimsPrincipal Data</h1>
<p>@authMessage</p>
<p>@surname</p>
@code {
private string? authMessage;
private string? surname;
private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();
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.
Pages/CascadeAuthState.razor :
razor
@page "/cascade-auth-state"
<p>@authMessage</p>
@code {
private string authMessage = "The user is NOT authenticated.";
[CascadingParameter]
private Task<AuthenticationState>? authenticationState { get; set; }
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.
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.
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
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.
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
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 ).
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.
razor
<AuthorizeView Policy="Over21">
<AuthorizeView Policy="LivesInCalifornia">
<p>You satisfy the 'Over21' and 'LivesInCalifornia' policies.
</p>
</AuthorizeView>
</AuthorizeView>
If neither Roles nor Policy is specified, AuthorizeView uses the default policy:
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.
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]
) 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.
razor
@page "/"
@attribute [Authorize(Roles = "Admin, Superuser")]
<p>You can only see this if you're in the 'Admin' or 'Superuser' role.</p>
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:
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.
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 ).
C#
using Microsoft.AspNetCore.Components;
using System.Linq;
C#
return false;
})
);
Pages/EditUser.razor :
razor
@page "/users/{id}/edit"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "EditUser")]
<h1>Edit User</h1>
@code {
[Parameter]
public string? Id { get; set; }
}
razor
<CascadingAuthenticationState>
<Router ...>
<Found ...>
<AuthorizeRouteView ...>
<NotAuthorized>
...
</NotAuthorized>
<Authorizing>
...
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView ...>
...
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
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.
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
@code {
[CascadingParameter]
private Task<AuthenticationState>? authenticationState { get; set; }
if (user.IsInRole("Admin"))
{
// ...
}
if ((await AuthorizationService.AuthorizeAsync(user,
"content-editor"))
.Succeeded)
{
// ...
}
}
}
}
}
Troubleshoot errors
Common errors:
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>
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
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.
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;
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.
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.
CustomAuthenticationStateProvider.cs :
C#
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
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
<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;
C#
using System.Security.Claims;
C#
builder.Services.AddScoped<AuthenticationService>();
C#
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
The following component's SignIn method creates a claims principal for the user's
identifier to set on AuthenticationService.CurrentUser :
razor
<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;
AuthenticationService.CurrentUser = newUser;
}
}
ExampleService.cs :
C#
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>();
razor
@page "/inject-auth-state-provider"
@inject AuthenticationStateProvider AuthenticationStateProvider
@inherits OwningComponentBase
<p>@message</p>
@code {
private string? message;
private ExampleService? ExampleService { get; set; }
message = await
ExampleService.ExampleMethod(AuthenticationStateProvider);
}
}
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.
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#
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.
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.
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:
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
CircuitOptions.DisconnectedCircuitMaxRetained
CircuitOptions.DisconnectedCircuitRetentionPeriod
CircuitOptions.JSInteropDefaultCallTimeout
CircuitOptions.MaxBufferedUnacknowledgedRenderBatches
HubConnectionContextOptions.MaximumReceiveMessageSize
For more information and configuration coding examples, see the following articles:
Browser events are dispatched from the client to the server in an asynchronous
fashion.
The server responds asynchronously rerendering the UI as necessary.
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.
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.
<p>Count: @count</p>
@code
{
private int count = 0;
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>
@code
{
private int count = 0;
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.
razor
@code {
private bool isLoading;
private Data[] data = Array.Empty<Data>();
The guard pattern demonstrated in the preceding example works if the background
operation is executed asynchronously with the async - await pattern.
razor
@implements IDisposable
...
@code {
private readonly CancellationTokenSource TokenSource =
new CancellationTokenSource();
if (TokenSource.Token.IsCancellationRequested)
{
return;
}
...
}
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.
Blazor Server doesn't ensure the integrity and confidentiality of the data sent between
the server and the client. Always use HTTPS.
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:
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.
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.
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:
This advice also applies when rendering links as part of the app:
For more information, see Prevent open redirect attacks in ASP.NET Core.
Security checklist
The following list of security considerations isn't comprehensive:
This article explains how to configure Blazor Server for additional security scenarios,
including how to pass tokens to a Blazor Server app.
7 Note
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#
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#
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
};
}
In the App component ( App.razor ), resolve the service and initialize it with the data
from the parameter:
razor
...
@code {
[Parameter]
public InitialApplicationState? InitialState { get; set; }
return base.OnInitializedAsync();
}
}
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;
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.
razor
...
<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>
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.
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.
UserService.cs :
C#
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;
public UserCircuitHandler(
AuthenticationStateProvider authenticationStateProvider,
UserService userService)
{
this.authenticationStateProvider = authenticationStateProvider;
this.userService = userService;
}
C#
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
...
builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
razor
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#
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) .
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.
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:
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:
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.
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:
_Imports.razor :
razor
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
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
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.
Prerendering support
Prerendering isn't supported for authentication endpoints ( /authentication/ path
segment).
For more information, see ASP.NET Core Blazor WebAssembly additional security
scenarios.
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.
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
...
...
}
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.
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.
General guidance for OIDC providers and the WebAssembly Authentication Library
Microsoft Accounts
Azure Active Directory (AAD)
Azure Active Directory (AAD) B2C
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.
7 Note
Visual Studio
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.
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"
}
}
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.
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.
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.
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 .
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
JSON
{
"Local": {
"Authority": "{AUTHORITY}",
"ClientId": "{CLIENT ID}"
}
}
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:
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:
7 Note
RedirectToLogin component
The RedirectToLogin component ( Shared/RedirectToLogin.razor ):
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:
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:
7 Note
Authentication component
The page produced by the Authentication component ( Pages/Authentication.razor )
defines the routes required for handling different authentication stages.
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
@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.
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).
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 .
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) .
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:
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).
.NET CLI
C#
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});
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.
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 .
Program.cs :
C#
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
});
wwwroot/appsettings.json configuration
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
}
}
C#
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
C#
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");
For more information, see the following sections of the Additional scenarios article:
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:
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
7 Note
RedirectToLogin component
The RedirectToLogin component ( Shared/RedirectToLogin.razor ):
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:
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:
7 Note
Authentication component
The page produced by the Authentication component ( Pages/Authentication.razor )
defines the routes required for handling different authentication stages.
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
@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.
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).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
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
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 .
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) .
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:
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).
.NET CLI
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.
C#
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes
.Add("https://graph.microsoft.com/User.Read");
});
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.
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.
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 .
Program.cs :
C#
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
});
wwwroot/appsettings.json configuration
{
"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
}
}
C#
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
C#
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");
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:
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
7 Note
RedirectToLogin component
The RedirectToLogin component ( Shared/RedirectToLogin.razor ):
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:
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
7 Note
Authentication component
The page produced by the Authentication component ( Pages/Authentication.razor )
defines the routes required for handling different authentication stages.
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
@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.
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).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
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
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 .
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) .
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:
Before proceeding with this article's guidance, confirm that you've selected the correct
directory for the AAD B2C tenant.
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).
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 ).
.NET CLI
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.
C#
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});
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.
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.
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 .
Program.cs :
C#
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C",
options.ProviderOptions.Authentication);
});
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
}
}
C#
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
C#
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");
For more information, see the following sections of the Additional scenarios article:
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:
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
7 Note
RedirectToLogin component
The RedirectToLogin component ( Shared/RedirectToLogin.razor ):
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:
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
7 Note
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
@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.
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).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
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
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 .
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) .
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:
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.
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. ).
) 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.
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 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:
.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) .
{PROJECT — BlazorSample
NAME}
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.
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:
using Microsoft.AspNetCore.Authentication.JwtBearer;
C#
builder.Services.Configure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.NameClaimType = "name";
});
appsettings.json configuration
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.
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
C#
app.UseAuthentication();
app.UseAuthorization();
WeatherForecast controller
This section pertains to the solution's Server app.
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
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.
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 .
Support for HttpClient instances is added that include access tokens when making
requests to the Server app.
Program.cs :
C#
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 default access token scopes represent the list of access token scopes that are:
C#
builder.Services.AddMsalAuthentication(options =>
{
...
options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
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:
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.
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:
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
7 Note
RedirectToLogin component
This section pertains to the solution's Client app.
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.
behaviors:
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
7 Note
Authentication component
This section pertains to the solution's Client app.
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.
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;
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
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
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}");
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 ).
) Important
The following configuration is not required when using the default App ID URI of
api://{SERVER API APP CLIENT ID} .
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.
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).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
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
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 .
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) .
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:
Before proceeding with this article's guidance, confirm that you've selected the correct
directory for the AAD B2C tenant.
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.
Select Expose an API from the sidebar and follow these steps:
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).
) 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:
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 ).
.NET CLI
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) .
{PROJECT — BlazorSample
NAME}
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).
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:
C#
using Microsoft.AspNetCore.Authentication.JwtBearer;
C#
builder.Services.Configure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.NameClaimType = "name";
});
appsettings.json configuration
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.
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) .
C#
app.UseAuthorization();
WeatherForecast controller
This section pertains to the solution's Server app.
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
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.
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 .
Support for HttpClient instances is added that include access tokens when making
requests to the server project.
Program.cs :
C#
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 default access token scopes represent the list of access token scopes that are:
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}");
});
C#
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE
URI}");
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.
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:
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:
7 Note
RedirectToLogin component
This section pertains to the solution's Client app.
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.
behaviors:
7 Note
Authentication component
This section pertains to the solution's Client app.
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
@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.
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:
@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;
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.
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).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
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
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 .
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) .
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
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:
Visual Studio
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.
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.
In Program.cs :
C#
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite( ... ));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
C#
builder.Services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
C#
builder.Services.AddAuthentication()
.AddIdentityServerJwt();
7 Note
In Program.cs :
The Identity Server Middleware exposes the OpenID Connect (OIDC) endpoints:
C#
app.UseIdentityServer();
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.
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.
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.
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.
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
C#
builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI",
client => client.BaseAddress = new
Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
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
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
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
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
7 Note
RedirectToLogin component
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
behaviors:
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
7 Note
Authentication 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
@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
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:
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;
CustomUserFactory.cs :
C#
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
if (roleClaims.Any())
{
foreach (var existingClaim in roleClaims)
{
identity.RemoveClaim(existingClaim);
}
var rolesElem =
account.AdditionalProperties[identity.RoleClaimType];
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;
}
}
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 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;
await Task.CompletedTask;
}
C#
using Duende.IdentityServer.Services;
...
builder.Services.AddTransient<IProfileService, ProfileService>();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
C#
User.Identity.Name is populated in the Client app with the user's user name, which is
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
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;
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
var rng = new Random();
if (user != null)
{
logger.LogInformation($"User.Identity.Name: {user.UserName}");
}
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:
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:
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
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.
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:
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.
PowerShell
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.
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).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
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
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 .
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) .
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.
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.
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>();
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
...
...
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
TryAddAdditionalParameter
TryRemoveAdditionalParameter
TryGetAdditionalParameter
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");
}
requestOptions.TryAddAdditionalParameter("prompt", "login");
requestOptions.TryAddAdditionalParameter("loginHint",
"peter@contoso.com");
Navigation.NavigateToLogin("authentication/login", requestOptions);
}
}
InteractiveRequestOptions
Popup request parameter list
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");
});
}
InteractiveRequestOptions
Redirect request parameter list
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#
Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl,
accessTokenResult.InteractionOptions);
}
InteractiveRequestOptions
Popup request parameter list
C#
Navigation.NavigateToLogout("authentication/logout", "goodbye");
C#
var loginPath =
RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath;
The presence of an @using / using statement for API in the following namespaces:
Microsoft.AspNetCore.Components.WebAssembly.Authentication
Microsoft.Extensions.Options
IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>
C#
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
In the preceding code, the scopes example.read and example.write are generic
examples not meant to reflect valid scopes for any particular provider.
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
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
...
@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.
C#
using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
...
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:
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;
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; ).
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.
razor
...
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:
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.
razor
...
@code {
protected override async Task OnInitializedAsync()
{
var client =
ClientFactory.CreateClient("WebAPI.NoAuthenticationClient");
...
}
}
7 Note
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.
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.
In a Razor component:
razor
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
...
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.
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 ).
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:
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.
razor
...
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject IJSRuntime JS
@inject NavigationManager Navigation
@code {
public class Profile
{
public string? Name { get; set; }
public string? LastName { get; set; }
}
if (currentQuery.Contains("state=resumeSavingProfile"))
{
User = await JS.InvokeAsync<Profile>("sessionStorage.getItem",
"resumeSavingProfile");
}
}
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;
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>
@code {
private int currentCount = 0;
C#
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
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; }
RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
Action))
{
AuthenticationState.Id = Guid.NewGuid().ToString();
await JS.InvokeVoidAsync("sessionStorage.setItem",
AuthenticationState.Id, State.GetStateForLocalStorage());
}
}
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 :
C#
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>
(options =>
{
builder.Configuration.Bind("AzureAd",
options.ProviderOptions.Authentication);
});
builder.Services.AddSingleton<StateContainer>();
Route Purpose
authentication/login- Displays error messages when the sign-in operation fails for some
failed reason.
authentication/logout- Displays error messages when the sign-out operation fails for
failed some reason.
In the following example, all of the paths are prefixed with /security .
razor
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@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"
You're allowed to break the UI into different pages if you choose to do so.
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>
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;
C#
using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
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>();
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#
Program.cs :
C#
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);
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>
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.
For more information, see Persist additional claims and tokens from external providers in
ASP.NET Core.
C#
In this scenario:
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.
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.
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)?.
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
<p>
<input @bind="name" placeholder="Type your name" />
<button @onclick="GetGreeting" class="btn btn-primary">Call gRPC
service</button>
</p>
@code {
private string name = "Bert";
private string? serverResponse;
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.
2 Warning
TypeScript
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.
diff
- <script
src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationServic
e.js"></script>
+ <script src="js/CustomAuthenticationService.js"></script>
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) .
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
) Important
The class's structure must match what the library expects when the JSON is
serialized with System.Text.Json.
C#
[JsonPropertyName("client_id")]
public string? ClientId { get; set; }
[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:
C#
Uri.TryCreate(
$"{builder.HostEnvironment.BaseAddress}authentication/login-
callback",
UriKind.Absolute, out var redirectUri);
options.RedirectUri = redirectUri;
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:
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:
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).
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.
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.
Roles : AAD App Roles array (covered in the App Roles section)
across tenants)
CustomUserAccount.cs :
C#
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
[JsonPropertyName("wids")]
public List<string>? Wids { get; set; }
[JsonPropertyName("oid")]
public string? Oid { get; set; }
}
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:
CustomAccountFactory.cs :
C#
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
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();
var requestMemberOf =
client.Users[account?.Oid].MemberOf;
var memberships = await
requestMemberOf.Request().GetAsync();
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.
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#
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.
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.
Pages/CheckPolicy.razor :
razor
@page "/checkpolicy"
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
<h1>Check Policy</h1>
@code {
private string policyMessage = "Check hasn't been made yet.";
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
if ((await AuthorizationService.AuthorizeAsync(user,
"BillingAdministrator")).Succeeded)
{
policyMessage = "Yes! The 'BillingAdministrator' policy is
met.";
}
else
{
policyMessage = "No! 'BillingAdministrator' policy is NOT met.";
}
}
}
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
{
...
}
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
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):
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
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.
AuthorizeView component
razor
<AuthorizeView Roles="Admin">
razor
Procedural logic
C#
if (user.IsInRole("Admin")) { ... }
Require that the user be in either the Admin or Developer role with the
AuthorizeView component:
razor
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
Require that the user be in both the Admin and Developer roles with the
[Authorize] attribute:
razor
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#
C#
[Authorize(Roles = "Admin")]
Procedural logic
C#
if (User.IsInRole("Admin")) { ... }
Require that the user be in either the Admin or Developer role with the
[Authorize] attribute:
C#
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#
...
[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#
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;
services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>
(
options =>
{
scopes?.ForEach((scope) =>
{
options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
});
});
services.AddScoped<IAuthenticationProvider,
GraphAuthenticationProvider>();
services.AddScoped(sp =>
{
return new GraphServiceClient(
baseUrl,
sp.GetRequiredService<IAuthenticationProvider>(),
sp.GetRequiredService<IHttpProvider>());
});
return services;
}
public GraphAuthenticationProvider(IAccessTokenProvider
tokenProvider,
IConfiguration config)
{
TokenProvider = tokenProvider;
this.config = config;
}
In Program.cs , add the Graph client services and configuration with the AddGraphClient
extension method:
C#
builder.Services.AddGraphClient(baseUrl, scopes);
Pages/GraphExample.razor :
razor
@page "/graph-example"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client
@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
<p>Mobile Phone: @user.MobilePhone</p>
}
@code {
private Microsoft.Graph.User? user;
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.
CustomAccountFactory.cs :
C#
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
return initialUser;
}
}
Configure the MSAL authentication to use the custom user account factory.
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#
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>();
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.
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.
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.
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 .
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.
frame-ancestors
report-to
report-uri
sandbox
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
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 .
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.
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.
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.
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.
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:
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:
localStorage is the better choice if the app must persist state across closing and re-
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.
7 Note
Protected Browser Storage relies on ASP.NET Core Data Protection and is only
supported for Blazor Server apps.
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#
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.
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#
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.
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#
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:
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
CSHTML
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;
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; }
7 Note
The CounterStateProvider component handles the loading phase by not rendering its
child content until state loading is complete.
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"
@code {
[CascadingParameter]
private CounterStateProvider? CounterStateProvider { get; set; }
To persist many different state objects and consume different subsets of objects in
different places, it's better to avoid persisting state globally.
) Important
StateContainer.cs :
C#
C#
builder.Services.AddSingleton<StateContainer>();
C#
builder.Services.AddScoped<StateContainer>();
C#
services.AddScoped<StateContainer>();
Shared/Nested.razor :
razor
@implements IDisposable
@inject StateContainer StateContainer
<h2>Nested component</h2>
<p>
<button @onclick="ChangePropertyValue">
Change the Property from the Nested component
</button>
</p>
@code {
protected override void OnInitialized()
{
StateContainer.OnChange += StateHasChanged;
}
Pages/StateContainerExample.razor :
razor
@page "/state-container-example"
@implements IDisposable
@inject StateContainer StateContainer
<p>
<button @onclick="ChangePropertyValue">
Change the Property from the State Container Example component
</button>
</p>
<Nested />
@code {
protected override void OnInitialized()
{
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
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
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.
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 .
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"
}
}
}
}
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
2. With the Server project selected in Solution Explorer, press F5 to run the app
in the debugger.
7 Note
7 Note
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.
While debugging a Blazor WebAssembly app, you can also debug server code:
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.
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.
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}"
JSON
"cwd": "${workspaceFolder}/blazorwasm"
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.
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.
7 Note
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.
XML
<DebuggerSupport>true</DebuggerSupport>
<CopyOutputSymbolsToPublishDirectory>true</CopyOutputSymbolsToPublishDir
ectory>
2 Warning
JSON
{
"name": "Attach and Debug"
"type": "blazorwasm",
"request": "attach",
"url": "{URL}"
}
7 Note
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.
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 .
+ 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).
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
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.
OnInitialized:
C#
protected override void OnInitialized()
{
#if DEBUG
Thread.Sleep(10000);
#endif
...
}
OnInitializedAsync:
C#
...
}
Console
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.
XML
<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="{ASSEMBLY NAME}.dll" />
</ItemGroup>
The {ASSEMBLY NAME} placeholder is the name of the assembly. The .dll file extension is
required.
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.
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.
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();
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.
For more information, see ASP.NET Core Blazor routing and navigation.
For more information, see ASP.NET Core Blazor routing and navigation.
OnNavigateAsync callback and the assembly names in the blazor.boot.json file are out
of sync.
To rectify this:
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;
C#
builder.Services.AddScoped<LazyAssemblyLoader>();
Complete example
The demonstration in this section:
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;
return true;
}
}
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>
<button type="submit">Submit</button>
</EditForm>
<p>
@message
</p>
@code {
private RobotModel robotModel = new() { AxisSelection = Axis.Left };
private string? message;
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 .
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>
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…</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();
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:
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.
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
Test.c :
int fact(int n)
{
if (n == 0) return 1;
return n * fact(n - 1);
}
XML
<ItemGroup>
<NativeFileReference Include="Test.c" />
</ItemGroup>
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.
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) .
.NET CLI
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 .
Pages/NativeDependencyExample.razor :
razor
@page "/native-dependency-example"
@using SkiaSharp
@using SkiaSharp.Views.Blazor
<PageTitle>Native dependency</PageTitle>
canvas.Clear(SKColors.White);
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
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
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; }
prevInboundFlightId = InboundFlight?.FlightId ?? 0;
prevOutboundFlightId = OutboundFlight?.FlightId ?? 0;
}
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.
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.
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.
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>
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
@RenderWelcomeInfo
@code {
private RenderFragment RenderWelcomeInfo = @<p>Welcome to your new app!
</p>;
}
razor
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>;
}
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#
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
For more information on generic type parameters ( @typeparam ), see the following
resources:
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
For more information, see ASP.NET Core Blazor cascading values and parameters.
<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.
For more information, see ASP.NET Core Blazor attribute splatting and arbitrary
parameters.
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; }
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>
@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();
}
await JS.InvokeVoidAsync("onThrottledMouseMove",
mouseMoveElement, selfReference, minInterval);
}
}
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>
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;
Task IHandleEvent.HandleEventAsync(
EventCallbackWorkItem callback, object? arg) =>
callback.InvokeAsync(arg);
}
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;
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;
Logger.LogInformation(
"This event handler doesn't trigger a rerender. " +
"Mouse coordinates: {ScreenX}:{ScreenY}",
args.ScreenX, args.ScreenY);
}
}
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";
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>
@code {
private string heading = "Select a button to learn its position";
button.Id = Guid.NewGuid().ToString();
Buttons.Add(button);
}
}
Additionally on Blazor Server, these calls are passed across the network.
C#
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#
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.
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");
}
}
razor
...
@inject IJSRuntime JS
@implements IAsyncDisposable
...
@code {
...
private IJSInProcessObjectReference? module;
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 .
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.
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.
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.
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.
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.
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>
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.
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.
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.
7 Note
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:
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>
@code {
private int currentCount = 0;
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");
}
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
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.
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.
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.
XML
...
<ServiceWorkerAssetsManifest>service-worker-
assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
XML
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js"
PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
Create a separate, new PWA project with the dotnet new command in a command
shell:
.NET CLI
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
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
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
HTML
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>
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
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 .
To share logic between the two service worker files, consider the following approach:
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.
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:
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 .
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.
start up your Blazor WebAssembly app. These initial requests are known as navigation
requests, as opposed to:
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
JavaScript
JavaScript
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
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.
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
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 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 .
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.
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.
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.
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.
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 .
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 CarChecker sample app demonstrates the preceding approaches. See the
following parts of the app:
OfflineAccountClaimsPrincipalFactory
( Client/Data/OfflineAccountClaimsPrincipalFactory.cs )
LocalVehiclesStore ( Client/Data/LocalVehiclesStore.cs )
Additional resources
Troubleshoot integrity PowerShell script
SignalR cross-origin negotiation for authentication
Host and deploy ASP.NET Core Blazor
Article • 05/19/2023
7 Note
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.
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:
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
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 :
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#
Configure the <base> tag, per the guidance in the Configure the app base path section.
Configure the <base> tag, per the guidance in the Configure the app base path
section.
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:
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.
HTML
<base href="/CoolApp/">
HTML
<base href="/CoolApp/">
C#
app.UsePathBase("/CoolApp");
Option 1: Use the <base> tag to set the app's base path (location of <head>
content):
HTML
<base href="/CoolApp/">
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"
}
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:
In Blazor WebAssembly web API requests with the HttpClient service, confirm that JSON
helpers (HttpClientJsonExtensions) do not prefix URLs with a forward slash ( / ):
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");
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
For a Blazor WebAssembly app with a relative URL path of /CoolApp/ ( <base
href="/CoolApp/"> ), the command is:
.NET CLI
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
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) .
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
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 .
C#
...
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("~/Admin/{*clientroutes:nonfile}",
"/Admin/_Host");
app.MapFallbackToPage("/_Host");
app.Run();
Deployment
For deployment guidance, see the following topics:
This article explains how to host and deploy a Blazor Server app using ASP.NET Core.
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:
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.
Failed to connect via WebSockets, using the Long Polling fallback transport. This
may be due to a VPN or proxy blocking the connection.
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
C#
builder.Services.AddServerSideBlazor()
.AddHubOptions(options =>
{
options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});
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
In the following example, a custom value of 60 seconds is used for the server
timeout.
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#
hubConnection.ServerTimeout = TimeSpan.FromSeconds(60);
hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);
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.
) Important
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.
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;
});
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:
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.
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 .
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.
Repeat the preceding settings for the key vault. Select the appropriate key vault
service and key in the Basics tab.
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"
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:
a2enmod proxy
a2enmod proxy_wstunnel
For more information and configuration guidance, consult the following resources:
Shared/MeasureLatency.razor :
razor
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@code {
private DateTime startTime;
private TimeSpan? latency;
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.
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:
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.
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.
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.
This article explains how to host and deploy Blazor WebAssembly using ASP.NET Core,
Content Delivery Networks (CDN), file servers, and GitHub Pages.
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 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.
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.
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
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.
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
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.
7 Note
HTML
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.
XML
<PropertyGroup>
<BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup>
.NET CLI
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/ ):
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.
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).
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
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).
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>
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
In the preceding command, the {RID} placeholder is the Runtime Identifier (RID).
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.
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.
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.
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.
IIS
IIS is a capable static file server for Blazor apps. To configure IIS to host Blazor, see Build
a Static Website on IIS.
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 ).
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 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>
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
XML
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.
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>
XML
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 .
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:
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.
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 / {
...
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:
<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).
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
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.
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
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
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
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
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 / {
...
}
}
}
http {
server {
...
location /blazor {
...
}
}
}
For more information and configuration guidance, consult the following resources:
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.
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
In the preceding command, the {PATH} placeholder is the path to the published
_framework folder (for example, .\bin\Release\net6.0\browser-
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
In the preceding command, the {PATH} placeholder is the path to the published
_framework folder (for example, .\bin\Release\net6.0\browser-
Console
In the preceding command, the {PATH} placeholder is the path to the published
service-worker-assets.js 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
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>
diff
diff
For more information on custom web.config files, see the Use a custom web.config
section.
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.
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.
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:
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) .
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.
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.
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:
Invoke the script with the following command in a PowerShell command shell:
PowerShell
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
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
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.
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>
.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
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
To disable integrity checking, remove the integrity parameter by changing the line to
the following:
JavaScript
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.
Failed to connect via WebSockets, using the Long Polling fallback transport. This
may be due to a VPN or proxy blocking the connection.
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
C#
builder.Services.AddSignalR(options =>
{
options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});
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
In the following example, a custom value of 60 seconds is used for the server
timeout.
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#
hubConnection.ServerTimeout = TimeSpan.FromSeconds(60);
hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);
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
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:
The approach demonstrated in this article serves as a starting point for developers to
devise their own strategies and custom loading processes.
2 Warning
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.
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:
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:
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
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; }
bundle.CopyToAsync(output).ConfigureAwait(false).GetAwaiter()
.GetResult();
output.Flush(true);
}
return true;
}
}
}
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:
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
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>
The JS initializers:
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNe
tCore.Components.WebAssembly.MultipartBundle.lib.module.js :
JavaScript
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);
}
}
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#
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 ).
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 .
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:
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.
The following table describes the solution's folders and project names after the
SecondClient folder and MultipleBlazorApps.SecondClient project are added.
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.
XML
<StaticWebAssetBasePath>FirstApp</StaticWebAssetBasePath>
XML
<StaticWebAssetBasePath>SecondApp</StaticWebAssetBasePath>
XML
<ItemGroup>
<ProjectReference
Include="..\Shared\MultipleBlazorApps.Shared.csproj" />
</ItemGroup>
XML
<ProjectReference
Include="..\SecondClient\MultipleBlazorApps.SecondClient.csproj" />
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
For example:
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:
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:
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");
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
C#
first.UseBlazorFrameworkFiles("/FirstApp");
first.UseStaticFiles();
first.UseStaticFiles("/FirstApp");
first.UseRouting();
first.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToFile("/FirstApp/{*path:nonfile}",
"FirstApp/index.html");
});
});
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
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.
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;
7 Note
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;
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.
) 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
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
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
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
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.
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.
By default, consider using one context per operation. The context is designed for
fast, low overhead instantiation:
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.
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.
razor
C#
Filters.Loading = false;
await ReloadAsync();
}
7 Note
razor
@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory
The sample app ensures the context is disposed when the component is disposed:
C#
C#
try
{
Context = DbFactory.CreateDbContext();
await base.OnInitializedAsync();
}
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
Shared/PetDetails.razor :
razor
<h2>Pet Details</h2>
<p>@PetDetailsQuote</p>
@code
{
[Parameter]
public string? PetDetailsQuote { get; set; }
}
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; }
2 Warning
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.
Imagine that someFlag becomes false and the markup is rendered again. This time, the
builder receives the sequence in the following table.
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:
C#
var seq = 0;
if (someFlag)
{
builder.AddContent(seq++, "First");
}
builder.AddContent(seq++, "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
This time, the diff algorithm sees that two changes have occurred. The algorithm
generates the following edit script:
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.
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
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:
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:
C#
builder.Services.AddControllersWithViews();
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();
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
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>
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
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"
}
}
}
}
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": {}
}
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 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 .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 .
JavaScript
const context = [
"/weatherforecast",
];
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.
.NET CLI
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 .
Console
cd ClientApp
npm install <package_name>
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.
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 .
.NET CLI
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.
Console
cd ClientApp
npm install <package_name>
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.
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.
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.
Microsoft.AspNetCore.NodeServices (NodeServices)
Microsoft.AspNetCore.SpaServices (SpaServices)
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.
Server-side prerendering
Webpack Dev Middleware
Hot Module Replacement
Routing helpers
To verify these components are installed and can be found, run the following
from the command line:
Console
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.
Console
npm i -S aspnet-prerendering
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>
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
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
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);
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
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);
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
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);
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.
JSON
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");
}
JavaScript
Console
npm i -D webpack-hot-middleware
C#
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true
});
The webpack.config.js file must define a plugins array, even if it's left empty:
JavaScript
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.
Console
npm i -S @angular/router
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.
These templates can be installed via the .NET Core CLI by running the following
command:
.NET CLI
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
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.
.NET CLI
.NET CLI
dotnet run
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
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
JavaScript
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
.NET CLI
By Scott Addie
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
.NET CLI
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
Console
libman --help
Console
1.0.163+g45474d37ed
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
Synopsis
Console
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
--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:
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": []
}
Synopsis
Console
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
--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
JSON
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.2.1",
"destination": "wwwroot/scripts/jquery",
"files": [
"jquery.min.js"
]
}
]
}
Console
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"
]
}
]
}
Synopsis
Console
Options
The following options are available for the libman restore command:
-h|--help
--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
Synopsis
Console
Options
The following options are available for the libman clean command:
-h|--help
--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
Deletes all files associated with the specified library from the destination in
libman.json .
If more than one library with the same name is installed, you're prompted to choose
one.
Synopsis
Console
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
--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/"
}
]
}
Console
Console
libman uninstall jquery@3.3.1
Console
If more than one library with the same name is installed, you're prompted to choose
one.
Synopsis
Console
Arguments
LIBRARY
Options
The following options are available for the libman update command:
-pre
-h|--help
--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
Console
Console
Synopsis
Console
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
--libraries
-h|--help
--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
Console
Console
Cache contents:
---------------
unpkg:
knockout
react
vue
cdnjs:
font-awesome
jquery
knockout
lodash.js
react
Console
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.
Console
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
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:
Prerequisites
Visual Studio 2019 with the ASP.NET and web development workload
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.
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
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
† 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
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" />
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.
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
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.
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
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.
Open libman.json .
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.
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:
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 wasm-tools workload, which brings in the related MSBuild targets.
.NET CLI
.NET CLI
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:
XML
<TargetFramework>net7.0</TargetFramework>
XML
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
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
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>
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>
) Important
"Import" and "export" throughout this article are defined from the perspective of
.NET:
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:
) 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.
function is imported into C# and called by the C# method GetHRef . The GetHRef
method is shown later in this section.
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.
JS module:
JavaScript
import { dotnet } from './dotnet.js'
setModuleImports("main.js", {
window: {
location: {
href: () => globalThis.window.location.href
}
}
});
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.
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
Func<TResult> Function
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
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 .
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;
}
.NET CLI
dotnet workload install wasm-experimental
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
Build the app from Visual Studio or by using the .NET CLI:
.NET CLI
dotnet 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
.NET CLI
Build the app from Visual Studio or by using the .NET CLI:
.NET CLI
dotnet 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:
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
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:
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
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;
}
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"
}
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
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
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.
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 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.
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
JavaScript
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
This article explains the benefits of applying bundling and minification, including how
these features can be used with ASP.NET Core web apps.
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
JavaScript
AddAltToImg=function(t,a){var
r=$(t,a);r.attr("alt",r.attr("id").replace(/ID/,""))};
Original Renamed
imageTagAndImageID t
imageContext a
imageElement r
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.
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.
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
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:
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();
}
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.
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.
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
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
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.
Cookies HTTP cookies. May include data stored using server-side app code.
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:
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:
The following code shows how to set up the in-memory session provider with a default
in-memory implementation of IDistributedCache :
C#
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;
});
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 order of middleware is important. Call UseSession after UseRouting and before
MapRazorPages and MapDefaultControllerRoute . See Middleware Ordering.
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.
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 ).
C#
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;
});
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.
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.
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#
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.
C#
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;
}
}
}
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#
[TempData]
public string Message { get; set; }
[BindProperty]
public Customer Customer { get; set; }
_context.Customer.Add(Customer);
await _context.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";
return RedirectToPage("./IndexPeek");
}
}
CSHTML
@page
@model IndexModel
<h1>Peek Contacts</h1>
@{
if (TempData.Peek("Message") != null)
{
<h3>Message: @TempData.Peek("Message")</h3>
}
}
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");
}
The following code displays TempData["Message"] , but at the end of the request,
TempData["Message"] is deleted:
CSHTML
@page
@model IndexModel
@{
if (TempData["Message"] != null)
{
<h3>Message: @TempData["Message"]</h3>
}
}
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.
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.
C#
builder.Services.AddRazorPages()
.AddSessionStateTempDataProvider();
builder.Services.AddControllersWithViews()
.AddSessionStateTempDataProvider();
builder.Services.AddSession();
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.
C#
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();
await _next(httpContext);
}
}
Other code can access the value stored in HttpContext.Items using the key exposed by
the middleware class:
C#
.TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey,
out var middlewareSetValue);
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.
Common errors
"Unable to resolve service for type
'Microsoft.Extensions.Caching.Distributed.IDistributedCache' while attempting to
activate 'Microsoft.AspNetCore.Session.DistributedSessionStore'."
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.
Additional resources
View or download sample code (how to download)
Pages and views frequently share visual and programmatic elements. This article
demonstrates how to:
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:
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:
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>
<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>
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
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).
HTML
@section Scripts {
<script type="text/javascript" src="~/scripts/main.js"></script>
}
HTML
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
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.
@addTagHelper
@removeTagHelper
@tagHelperPrefix
@using
@model
@inherits
@inject
@namespace
The file doesn't support other Razor features, such as functions and section definitions.
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.
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:
@inject : for each property, the closest one to the view overrides any others with
the same property name
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
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
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.
CSHTML
<p>@@Username</p>
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>
HTML
@{
string message = "foreignObject example with Scalable Vector Graphics
(SVG)";
}
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
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.
CSHTML
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
HTML
@{
var joe = new Person("Joe", 33);
}
<p>Age@(joe.Age)</p>
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>")
HTML
<span>Hello World</span>
<span>Hello World</span>
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>")
HTML
<span>Hello World</span>
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>
HTML
CSHTML
@{
void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}
HTML
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>
}
CSHTML
CSHTML
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.
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" />
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:
CSHTML
@if (value % 2 == 0)
{
<p>The value was even.</p>
}
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>
}
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;
}
CSHTML
@{
var people = new Person[]
{
new Person("Weston", 33),
new Person("Johnathon", 41),
...
};
}
@for
CSHTML
CSHTML
@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>
}
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 -->
HTML
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
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";
}
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
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)
}
@functions
The @functions directive enables adding C# members (fields, properties, and methods)
to the generated class:
CSHTML
@functions {
// C# members (fields, properties, and methods)
}
For example:
CSHTML
@functions {
public string GetHello()
{
return "Hello";
}
}
HTML
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
CSHTML
@{
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}
@functions {
private void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}
}
HTML
@implements
The following example implements System.IDisposable so that the Dispose method can
be called:
CSHTML
@implements IDisposable
<h1>Example</h1>
@functions {
private bool _isDisposed;
...
@inherits
The @inherits directive provides full control of the class the view inherits:
CSHTML
@inherits TypeNameOfClassToInheritFrom
C#
using Microsoft.AspNetCore.Mvc.Razor;
CSHTML
@inherits CustomRazorPage<TModel>
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>
CSHTML
@inherits CustomRazorPage<TModel>
If "rick@contoso.com" is passed in the model, the view generates the following HTML
markup:
HTML
@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
@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
C#
Razor exposes a Model property for accessing the model passed to the view:
CSHTML
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
CSHTML
@namespace Your.Namespace.Here
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
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
When set to false (default), whitespace in the rendered markup from Razor
components ( .razor ) is removed if:
@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>
@attributes
@bind
Data binding in components is accomplished with the @bind attribute. For more
information, see ASP.NET Core Blazor data binding.
@bind:culture
@on{EVENT}
Razor provides event handling features for components. For more information, see
ASP.NET Core Blazor event handling.
@on{EVENT}:preventDefault
@key
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
@typeparam
The @typeparam directive declares a generic type parameter for the generated
component class:
razor
@typeparam TEntity
razor
CSHTML
@<tag>...</tag>
C#
CSHTML
@{
Func<dynamic, object> petTemplate = @<p>You have a pet named
<strong>@item.Name</strong>.</p>;
CSHTML
HTML
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();
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 ).
Directive Function
@tagHelperPrefix Specifies a tag prefix to enable Tag Helper support and to make Tag Helper
usage explicit.
Razor keywords
page
namespace
functions
inherits
model
section
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.
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:
Developers are encouraged to match the casing of file and directory names to the
casing of:
Matching case ensures the deployments find their views regardless of the underlying file
system.
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 .
The Razor class library (RCL) template defaults to Razor component development
by default. The Support pages and views option supports pages and views.
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 .
If the RCL uses Razor Pages, enable the Razor Pages services and endpoints in the
hosting app:
C#
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
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();
RazorUIClassLib/Pages
RazorUIClassLib/Pages/Shared
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";
}
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 .
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:
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>
<PrepareForBuildDependsOn>
CompileTypeScript;
GetTypeScriptOutputForPublishing;$(PrepareForBuildDependsOn)
</PrepareForBuildDependsOn>
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#
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
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#
builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();
builder.Services.AddRazorPages();
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 ).
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)
By Peter Kellner
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.
Cache
Component
Distributed Cache
Environment
Form
Form Action
Image
Input
Label
Link
Partial
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
Tag Helpers aren't supported in Razor components. For more information, see ASP.NET
Core Razor components.
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.
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#
CSHTML
<label asp-for="Movie.Title"></label>
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.
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
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.
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.
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.
CSHTML
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.
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 .
CSHTML
<input asp-for="LastName"
@(Model?.LicenseId == null ? "disabled" : string.Empty) />
CSHTML
<input asp-for="LastName"
disabled="@(Model?.LicenseId == null)" />
C#
public class AppendVersionTagHelperInitializer :
ITagHelperInitializer<ScriptTagHelper>
{
public void Initialize(ScriptTagHelper helper, ViewContext context)
{
helper.AppendVersion = true;
}
}
C#
builder.Services.AddSingleton
<ITagHelperInitializer<ScriptTagHelper>,
AppendVersionTagHelperInitializer>();
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:
CSHTML
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.
CSHTML
With the Tag Helper version, as soon as you enter <l in the Visual Studio editor,
IntelliSense displays matching elements:
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.
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.
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.
Cache
Component
Distributed Cache
Environment
Form
Form Action
Image
Input
Label
Link
Partial
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
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.
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 overridden Process method controls what the tag helper does when
executed. The TagHelper class also provides an asynchronous version
( ProcessAsync ) with the same parameters.
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#
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.
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.
C#
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.
C#
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.
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#
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.
C#
[HtmlAttributeName("recipient")]
public string? MailTo { get; set; }
HTML
<email recipient="…"/>
ProcessAsync
In this section, we'll write an asynchronous email helper.
C#
Notes:
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.
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>");
}
}
}
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>
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.
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#
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")]
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; }
}
}
C#
using System;
using AuthoringTagHelpers.Models;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AuthoringTagHelpers.TagHelpers
{
public class WebsiteInformationTagHelper : TagHelper
{
public WebsiteContext Info { get; set; }
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.)
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>
7 Note
HTML
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
C#
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = nameof(Condition))]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; }
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#
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; }
The nameof operator will protect the code should it ever be refactored (we might
want to change the name to RedCondition ).
Because these two helpers are closely related and you may refactor them in the future,
we'll keep them in the same file.
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
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 the tag helper renders the anchor correctly.
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#
[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();
7 Note
In the first edition of the auto-linking tag helpers, you got the content of the
target with the following code:
C#
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#
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#
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.
C#
// 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.
C#
public class MinifiedVersionPartialTagHelper : PartialTagHelper
{
public MinifiedVersionPartialTagHelper(ICompositeViewEngine
viewEngine,
IViewBufferScope viewBufferScope)
: base(viewEngine, viewBufferScope)
{
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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
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 .
Use cases
Two common use cases of Tag Helper Components include:
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"" />";
return Task.CompletedTask;
}
}
}
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.
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;
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
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:
C#
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddTransient<ITagHelperComponent,
AddressScriptTagHelperComponent>();
services.AddTransient<ITagHelperComponent,
AddressStyleTagHelperComponent>();
}
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>";
}
C#
C#
C#
using System;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesSample.TagHelpers;
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));
}
}
Create a Component
To create a custom Tag Helper Component:
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#
The preceding ProcessAsync method injects the HTML provided to SetHtmlContent into
the matching <address> element. The injection occurs when:
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
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.
C#
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
[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();
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>
HTML
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
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
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.
C#
[Route("Speaker/{id:int}")]
public IActionResult Detail(int id) =>
View(Speakers.FirstOrDefault(a => a.SpeakerId == id));
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
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>
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
The Anchor Tag Helper generates a route directly to that controller action using the URL
/Speaker/Evaluations. The generated HTML:
HTML
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.
@{
var parms = new Dictionary<string, string>
{
{ "speakerId", "11" },
{ "currentYear", "true" }
};
}
<a asp-route="speakerevalscurrent"
asp-all-route-data="parms">Speaker Evaluations</a>
HTML
<a href="/Speaker/EvaluationsCurrent?speakerId=11¤tYear=true">Speaker
Evaluations</a>
C#
[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
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.
{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>
HTML
C#
services.AddMvc()
.AddRazorPagesOptions(options => options.AllowAreas =
true);
Usage in MVC
{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
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}");
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>
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>
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
HTML
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>
HTML
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.
C#
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>
HTML
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.
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.
enabled
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
The following example caches the contents of the Cache Tag Helper until 5:02 PM on
January 29, 2025:
CSHTML
expires-after
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
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
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
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
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
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
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
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#
Index.cshtml :
CSHTML
<cache vary-by="@Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
priority
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 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.
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.
Blazor WebAssembly app render modes are shown in the following table.
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.
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.
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
...
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; }
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
...
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 ).
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.
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.
MyClass.cs :
C#
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";
}
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.
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.
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
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>
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
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.
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.
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
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
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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:
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.
CSHTML
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
Additional resources
Cache in-memory in ASP.NET Core
Tag Helpers in forms in ASP.NET Core
Article • 03/08/2023
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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>
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.
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
Overview
The Partial Tag Helper is used for rendering a partial view in Razor Pages and MVC apps.
Consider that it:
@await Html.PartialAsync
@await Html.RenderPartialAsync
@Html.Partial
@Html.RenderPartial
C#
namespace TagHelpersBuiltIn.Models
{
public class Product
{
public int Number { get; set; }
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.
CSHTML
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.
CSHTML
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; }
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;
}
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>
CSHTML
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
Prerequisites
Follow the guidance in the Configuration section for either:
Blazor WebAssembly
Blazor Server
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) .
CSHTML
<body>
<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
...
<persist-component-state />
</body>
<body>
<component type="typeof(App)" render-mode="ServerPrerendered" />
...
<persist-component-state />
</body>
The {TYPE} placeholder represents the type of data to persist (for example,
WeatherForecast[] ).
razor
@implements IDisposable
@inject PersistentComponentState ApplicationState
...
@code {
private {TYPE} data;
private PersistingComponentStateSubscription persistingSubscription;
if (!ApplicationState.TryTakeFromJson<{TYPE}>(
"{TOKEN}", out var restored))
{
data = await ...;
}
else
{
data = restored!;
}
}
return Task.CompletedTask;
}
void IDisposable.Dispose()
{
persistingSubscription.Dispose();
}
}
For more information and a complete example, see Prerender and integrate ASP.NET
Core Razor components.
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.
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.
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.
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
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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
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.
Generates the HTML <FORM> action attribute value for a MVC controller
action or named route
Sample:
CSHTML
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.
CSHTML
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
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.
Attribute Description
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>
<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
CSHTML
<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
HTML
<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
C#
CSHTML
<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
HTML
<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
Syntax:
CSHTML
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
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).
Bool type="checkbox"
String type="text"
DateTime type="datetime-local"
Byte type="number"
Int 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):
[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
HTML
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.
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>
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.
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
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
CSHTML
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">
HTML
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:
C#
C#
[DataType(DataType.Password)]
public string Password { get; set; }
CSHTML
@model RegisterAddressViewModel
HTML
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1"
value="">
C#
C#
The following Razor shows how you access a specific Color element:
CSHTML
@model Person
@{
var index = (int)ViewData["index"];
}
CSHTML
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
C#
CSHTML
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
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
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.
Generates the id and name attributes, and the data validation attributes from the
model for a <textarea> element.
Sample:
C#
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
CSHTML
@model DescriptionViewModel
HTML
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
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
HTML
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.
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.
Validation also takes place on the server. Clients may have JavaScript disabled and
some validation can only be done on the server side.
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
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
HTML
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
Sample:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes
it to the Index view.
C#
C#
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg });
}
CSHTML
@model CountryViewModel
HTML
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
Sample:
C#
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
}
}
CSHTML
@model CountryEnumViewModel
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
}
}
HTML
Option Group
The HTML <optgroup> element is generated when the view model contains one or
more SelectListGroup objects.
C#
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America"
};
var EuropeGroup = new SelectListGroup { Name = "Europe" };
HTML
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; }
CSHTML
@model CountryViewModelIEnumerable
HTML
CSHTML
@model CountryViewModel
CSHTML
@model CountryViewModel
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#
CSHTML
@model CountryViewModel
C#
HTML
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
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.
C#
services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
{ options.FileProviders.Add(new EmbeddedFileProvider(assembly)); });
}
C#
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.
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
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;
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
By Steve Smith
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.
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
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.
IApplicationModelConvention
IControllerModelConvention
IActionModelConvention
IParameterModelConvention
For information on Razor Pages route and application model provider conventions,
see Razor Pages route and app conventions in ASP.NET Core.
C#
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace AppModelSample.Conventions
{
public class ApplicationDescription : IApplicationModelConvention
{
private readonly string _description;
C#
C#
C#
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace AppModelSample.Conventions
{
public class ControllerDescriptionAttribute : Attribute,
IControllerModelConvention
{
private readonly string _description;
C#
[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
public string Index()
{
return "Description: " +
ControllerContext.ActionDescriptor.Properties["description"];
}
C#
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace AppModelSample.Conventions
{
public class ActionDescriptionAttribute : Attribute,
IActionModelConvention
{
private readonly string _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"];
}
}
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;
}
}
}
C#
C#
options.Conventions.Add(new MustBeInRouteParameterModelConvention());
C#
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace AppModelSample.Conventions
{
public class CustomActionNameAttribute : Attribute,
IActionModelConvention
{
private readonly string _actionName;
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.
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?}"
};
}
}
C#
Tip
Add conventions to middleware via MvcOptions using the following approach. The
{CONVENTION} placeholder is the convention to add:
C#
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.";
}
}
}
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
Areas are an ASP.NET feature used to organize related functionality into a group as a
separate:
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.
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.
Controllers with the [Area] attribute to associate the controller with the area:
C#
[Area("Products")]
public class ManageController : Controller
{
C#
builder.Services.AddControllersWithViews();
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();
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
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();
}
{area:...} can be used as a token in route templates if url space is uniform across all
areas:
C#
builder.Services.AddControllersWithViews();
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 :
The following code uses MapAreaControllerRoute to create two named area routes:
C#
builder.Services.AddControllersWithViews();
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();
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 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.
_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:
The _ViewImports.cshtml file typically contains Tag Helpers imports, @using , and @inject
statements. For more information, see Importing Shared Directives.
C#
using Microsoft.AspNetCore.Mvc.Razor;
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();
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();
Project name
Areas
Products
Pages
_ViewImports
About
Index
Services
Pages
Manage
About
Index
CSHTML
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>
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 .
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>
The fully qualified class name must be used to specify the model ( @model
RPareas.Areas.Services.Pages.Manage.AboutModel ).
In the sample download, the Products area contains the following _ViewImports.cshtml
file:
CSHTML
@namespace RPareas.Areas.Products.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
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.
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
Filters in ASP.NET Core allow code to run before or after specific stages in the request
processing pipeline.
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:
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#
C#
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.
C#
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#
// ...
Use a tool such as the browser developer tools to examine the headers. Under
Response Headers, filter-header: Filter Value is displayed.
C#
// ...
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:
C#
As a result of filter nesting, the after code of filters runs in the reverse order of the before
code. The filter sequence:
The following example illustrates the order in which filter methods run for synchronous
action filters:
1 Global OnActionExecuting
2 Controller OnActionExecuting
Sequence Filter scope Filter method
3 Action OnActionExecuting
4 Action OnActionExecuted
5 Controller OnActionExecuted
6 Global OnActionExecuted
C#
[SampleActionFilter]
public class ControllerFiltersController : Controller
{
public override void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine(
$"- {nameof(ControllerFiltersController)}.
{nameof(OnActionExecuting)}");
base.OnActionExecuting(context);
}
base.OnActionExecuted(context);
}
public IActionResult Index()
{
Console.WriteLine(
$"- {nameof(ControllerFiltersController)}.{nameof(Index)}");
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.
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.
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);
});
C#
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#
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:
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.
ServiceFilterAttribute
TypeFilterAttribute
IFilterFactory implemented on the attribute.
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:
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 LoggingResponseHeaderFilterService(
ILogger<LoggingResponseHeaderFilterService> logger) =>
_logger = logger;
context.HttpContext.Response.Headers.Add(
nameof(OnResultExecuting),
nameof(LoggingResponseHeaderFilterService));
}
C#
builder.Services.AddScoped<LoggingResponseHeaderFilterService>();
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.
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.
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.
C#
[TypeFilter(typeof(LoggingResponseHeaderFilter),
Arguments = new object[] { "Filter-Header", "Filter Value" })]
public IActionResult WithTypeFilter() =>
Content($"- {nameof(FilterDependenciesController)}.
{nameof(WithTypeFilter)}");
Authorization filters
Authorization filters:
Resource filters
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.
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:
C#
C#
7 Note
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:
The following sample exception filter displays details about exceptions that occur when
the app is in development:
C#
C#
[TypeFilter(typeof(SampleExceptionFilter))]
public class ExceptionController : Controller
{
public IActionResult Index() =>
Content($"- {nameof(ExceptionController)}.{nameof(Index)}");
}
Exception filters:
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:
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.
C#
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:
C#
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#
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.
2 Warning
C#
C#
[ResponseHeaderFilterFactory]
public IActionResult Index() =>
Content($"- {nameof(FilterFactoryController)}.{nameof(Index)}");
C#
public
InternalSampleActionFilter(ILogger<InternalSampleActionFilter> logger) =>
_logger = logger;
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation(
$"- {nameof(InternalSampleActionFilter)}.
{nameof(OnActionExecuting)}");
}
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.
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#
await next();
});
}
}
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
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
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.
Property Description
For a web app, ensure your app is targeting the Microsoft.NET.Sdk.Web SDK.
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:
View components are intended anywhere reusable rendering logic that's too complex
for a partial view, such as:
Like controllers, a view component can be a POCO, but most developers take advantage
of the methods and properties available by deriving from 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.
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:
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:
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.
C#
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;
builder.Services.AddControllersWithViews()
.AddRazorOptions(options =>
{
options.ViewLocationFormats.Add("/{0}.cshtml");
});
builder.Services.AddDbContext<ToDoContext>(options =>
options.UseInMemoryDatabase("db"));
In the preceding code, the placeholder {0} represents the path Components/{View
Component Name}/{View Name} .
CSHTML
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>
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.
CSHTML
@await Component.InvokeAsync("PriorityList",
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
In the following example, the view component is called directly from the controller:
C#
C#
using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;
namespace ViewComponentSample.Controllers;
public class ToDoController : Controller
{
private readonly ToDoContext _ToDoContext;
C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;
namespace ViewComponentSample.ViewComponents;
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.
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#
C#
CSHTML
@model IEnumerable<ViewComponentSample.Models.TodoItem>
If the PVC view isn't rendered, verify the view component with a priority of 4 or higher is
called.
txt
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.
C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;
namespace ViewComponentSample.ViewComponents;
CSHTML
</table>
<div>
Testing nameof(PriorityList) <br />
CSHTML
</table>
<div>
Testing typeof(PriorityList) <br />
C#
using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;
namespace ViewComponentSample.ViewComponents
{
public class PriorityListSync : ViewComponent
{
private readonly ToDoContext db;
CSHTML
<div>
Testing nameof(PriorityList) <br />
IViewComponentHelper
Tag Helper
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
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:
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
C#
builder.Services.AddRazorPages()
.AddRazorRuntimeCompilation();
C#
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:
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:
C#
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#
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
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.
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.
Security considerations
Use caution when providing users with the ability to upload files to a server. Attackers
may attempt to:
2 Warning
Uploading malicious code to a system is frequently the first step to executing code
that can:
For information on reducing the attack surface area when accepting files from
users, see the following resources:
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.
For more information, see Quickstart: Use .NET to create a blob in object storage.
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.
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.
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.
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 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>
<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 = '/';
}
To perform the form POST in JavaScript for clients that don't support the Fetch API ,
use one of the following approaches:
<script>
"use strict";
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
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:
The following code removes the path from the file name:
C#
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.
C#
C#
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
GetTempFileNameA function
GetTempFileName
C#
Specify a page model property for the class that includes an IFormFile:
C#
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
7 Note
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#
_dbContext.File.Add(file);
await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}
return Page();
}
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
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#
context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
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>();
}
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.
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);
}
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);
}
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();
}
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.
_context.File.Add(file);
await _context.SaveChangesAsync();
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.");
}
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);
}
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.
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_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();
}
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.
Content validation
Use a third party virus/malware scanning API on uploaded content.
C#
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// The extension is invalid ... discontinue processing the file
}
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.
Razor automatically HTML encodes property values for display. The following code is
safe to use:
CSHTML
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
}
C#
...
}
C#
When using an <input> element, the name attribute is set to the value
battlePlans :
HTML
When using FormData in JavaScript, the name is set to the value battlePlans :
JavaScript
C#
C#
C#
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#
C#
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
{
...
}
CSHTML
@attribute [RequestSizeLimitAttribute(52428800)]
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.
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.
Additional resources
HTTP connection request draining
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:
XML
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- omitted for brevity -->
</Project>
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
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
aspnet-codegenerator is only required to scaffold from the command line, it's not
needed to use scaffolding with Visual Studio.
command installs the latest stable version of the dotnet aspnet-codegenerator tool:
.NET CLI
.NET CLI
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
Description
The dotnet aspnet-codegenerator global command runs the ASP.NET Core code
generator and scaffolding engine.
Arguments
generator
Generator Operation
Options
-n|--nuget-package-dir
-c|--configuration {Debug|Release}
-tfm|--target-framework
Target Framework to use. For example, net46 .
-b|--build-base-path
-h|--help
--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.
Areas
AreaNameToGenerate
Controllers
Data
Models
Views
Controller options
The following table lists options for aspnet-codegenerator razorpage , controller and
view :
Option Description
--dataContext or -dc The DbContext class to use or the name of the class to generate.
--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.
Option Description
--restWithNoViews Generate a Controller with REST style API. noViews is assumed and any
or -api view related options are ignored.
.NET CLI
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
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
--dataContext or -dc The DbContext class to use or the name of the class to generate.
--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.
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
.NET CLI
Empty
Create
Edit
Delete
Details
List
For example, the following command uses the Edit template to generate MyEdit.cshtml :
.NET CLI
The following table lists options for aspnet-codegenerator razorpage , controller and
view :
Option Description
--dataContext or -dc The DbContext class to use or the name of the class to generate.
--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.
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
.NET CLI
Identity
See Scaffold Identity
Choose between controller-based APIs
and minimal APIs
Article • 04/11/2023
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.
C#
namespace APIWithControllers;
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"
};
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;
app.UseHttpsRedirection();
app.Run();
}
}
C#
namespace APIWithControllers;
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:
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);
Method Notes
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);
Attribute Notes
[HttpGet] Identifies an action that supports the HTTP GET action verb.
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:
C#
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
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();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
C#
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
C#
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
ASP.NET Core MVC uses the ModelStateInvalidFilter action filter to do the preceding
check.
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."
]
}
}
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.
C#
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// To preserve the default behavior, capture the original delegate to
call later.
var builtInFactory = options.InvalidModelStateResponseFactory;
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
using Microsoft.AspNetCore.Mvc;
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";
});
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
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:
Container.
[FromBody] is inferred for complex type parameters not registered in the DI
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] 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:
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)
C#
[HttpPost]
public IActionResult Action3([FromBody] Product product, [FromBody]
Order order)
[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.
C#
using Microsoft.AspNetCore.Mvc;
builder.Services.AddControllers();
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.DisableImplicitFromServicesParameters = true;
});
app.MapControllers();
app.Run();
C#
using Microsoft.AspNetCore.Mvc;
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;
});
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
C#
using Microsoft.AspNetCore.Mvc;
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";
});
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
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"
}
C#
using Microsoft.AspNetCore.Mvc;
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";
});
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
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
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:
GET /api/todoitems Get all to-do items None Array of to-do items
Visual Studio 2022 with the ASP.NET and web development workload.
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 .
Visual Studio
Visual Studio displays the following dialog when a project is not yet configured to
use SSL:
The Swagger page /swagger/index.html is displayed. Select GET > Try it out > Execute.
The page displays:
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.
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"
}
]
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;
Model classes can go anywhere in the project, but the Models folder is used by
convention.
Visual Studio
C#
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models;
C#
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Scaffold a controller
Visual Studio
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.
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.
C#
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute.
The method gets the value of the TodoItem from the body of the HTTP request.
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.
In the Swagger browser window, select GET /api/TodoItems/{id}, and then select
Try it out.
GET /api/todoitems
GET /api/todoitems/{id}
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.
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.
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
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.
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.
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 .
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();
}
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.
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.
C#
namespace TodoApi.Models;
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;
// 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();
}
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();
}
Duende Identity Server is an OpenID Connect and OAuth 2.0 framework for ASP.NET
Core. Duende Identity Server enables the following security features:
) 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.
This tutorial creates a web API that runs Create, Read, Update, and Delete (CRUD)
operations on a MongoDB NoSQL database.
" 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:
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
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 .
2. In the command shell connect to the default test database by running the
following command:
Console
mongosh
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.
Console
db.createCollection('Books')
Console
{ "ok" : 1 }
5. Define a schema for the Books collection and insert two documents using the
following command:
Console
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.
db.Books.find().pretty()
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.
2. Select the ASP.NET Core Web API project type, 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
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;
[BsonElement("Name")]
public string BookName { get; set; } = null!;
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 .
JSON
{
"BookStoreDatabase": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BookStore",
"BooksCollectionName": "Books"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
C#
namespace BookStoreApi.Models;
C#
C#
using BookStoreApi.Models;
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 BooksService(
IOptions<BookStoreDatabaseSettings> bookStoreDatabaseSettings)
{
var mongoClient = new MongoClient(
bookStoreDatabaseSettings.Value.ConnectionString);
_booksCollection = mongoDatabase.GetCollection<Book>(
bookStoreDatabaseSettings.Value.BooksCollectionName);
}
C#
builder.Services.AddSingleton<BooksService>();
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);
_booksCollection = mongoDatabase.GetCollection<Book>(
bookStoreDatabaseSettings.Value.BooksCollectionName);
}
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;
[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);
[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;
[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();
}
}
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"
}
]
JSON
{
"id": "61a6058e6c43f32854e51f52",
"bookName": "Clean Code",
"price": 43.15,
"category": "Computers",
"author": "Robert C. Martin"
}
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 .
C#
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 .
C#
[BsonElement("Name")]
[JsonPropertyName("Name")]
public string BookName { get; set; } = null!;
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.
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
The two main OpenAPI implementations for .NET are Swashbuckle and NSwag , see:
In short:
OpenAPI is a specification.
Swagger is tooling that uses the OpenAPI specification. For example,
OpenAPIGenerator and SwaggerUI.
{
"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
Package installation
Swashbuckle can be added with the following approaches:
Visual Studio
PowerShell
C#
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
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.
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;
});
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")
}
});
});
Visual Studio
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;
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.
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"
}
}
},
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();
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
}
},
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.
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();
The Swagger UI now clearly documents the expected HTTP response codes:
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.
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
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.
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
PowerShell
Install-Package NSwag.AspNetCore
C#
C#
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:
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.
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
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#
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"
};
};
});
Visual Studio
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();
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
Installation
To install Microsoft.dotnet-openapi , run the following command:
.NET CLI
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
The preceding reference is required for the app to call the generated client code.
Add File
Options
Arguments
source-file The source to create a reference from. Must be an dotnet openapi add file
OpenAPI file. .\OpenAPI.json
Add URL
Options
-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
Arguments
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
Arguments
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
Arguments
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.
C#
[HttpGet("syncsale")]
public IEnumerable<Product> GetOnSaleProducts()
{
var products = _productContext.Products.OrderBy(p => p.Name).ToList();
C#
[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
var products = _productContext.Products.OrderBy(p =>
p.Name).AsAsyncEnumerable();
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
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);
}
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();
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(); .
C#
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#
[Required]
public string Name { get; set; } = string.Empty;
[Required]
public string Description { get; set; } = string.Empty;
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:
C#
[HttpGet]
public ActionResult<IEnumerable<Product>> Get() =>
_repository.GetProducts();
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;
}
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();
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.
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.
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);
}
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>().
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();
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:
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.
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);
}
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();
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:
C#
builder.Services.AddControllers()
.AddNewtonsoftJson();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
AddRazorPages
AddControllersWithViews
AddControllers
Update Program.cs :
C#
using JsonPatchSample;
using Microsoft.AspNetCore.Mvc.Formatters;
builder.Services.AddControllers(options =>
{
options.InputFormatters.Insert(0,
MyJPIF.GetJsonPatchInputFormatter());
});
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Options;
namespace JsonPatchSample;
return builder
.GetRequiredService<IOptions<MvcOptions>>()
.Value
.InputFormatters
.OfType<NewtonsoftJsonPatchInputFormatter>()
.First();
}
}
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
}
]
}
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
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
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.
move Same as remove from source followed by add to destination using value from
source.
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);
}
This code from the sample app works with the following Customer model:
C#
namespace JsonPatchSample.Models;
C#
namespace JsonPatchSample.Models;
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 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 following sample patch document sets CustomerName to null and deletes Orders[0] :
JSON
[
{
"op": "remove",
"path": "/customerName"
},
{
"op": "remove",
"path": "/orders/0"
}
]
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
}
}
]
JSON
[
{
"op": "move",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "move",
"from": "/orders/1",
"path": "/orders/0"
}
]
JSON
[
{
"op": "copy",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "copy",
"from": "/orders/1",
"path": "/orders/0"
}
]
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"
}
]
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.
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.
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:
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 .
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);
}
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.
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.
If no formatter is found that can satisfy the client's request, ASP.NET Core:
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.
This approach provides a more consistent experience across browsers when consuming
APIs.
C#
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.
C#
builder.Services.AddControllers()
.AddXmlSerializerFormatters();
When using the preceding code, controller methods return the appropriate format
based on the request's Accept header.
C#
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 });
C#
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 :
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:
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() });
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#
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."
]
}
}
camelCase by default:
C#
builder.Services.AddControllers();
builder.Services.Configure<MvcOptions>(options =>
{
options.ModelMetadataDetailsProviders.Add(
new SystemTextJsonValidationMetadataProvider());
});
To set a custom name for a property within a model, use the [JsonPropertyName]
attribute on the property:
C#
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."
]
}
}
C#
builder.Services.AddControllers()
.AddNewtonsoftJson();
builder.Services.Configure<MvcOptions>(options =>
{
options.ModelMetadataDetailsProviders.Add(
new NewtonsoftJsonValidationMetadataProvider());
});
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
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.
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 .
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;
[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
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.
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.
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#
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
var logger =
serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
var buffer = new StringBuilder();
await httpContext.Response.WriteAsync(buffer.ToString(),
selectedEncoding);
}
C#
For binary types, derive from the InputFormatter or OutputFormatter base class.
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.
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:
C#
var logger =
serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
var buffer = new StringBuilder();
await httpContext.Response.WriteAsync(buffer.ToString(),
selectedEncoding);
}
C#
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#
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
var logger =
serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();
try
{
await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
await ReadLineAsync("VERSION:", reader, context, logger);
context.ModelState.TryAddModelError(context.ModelName,
errorMessage);
logger.LogError(errorMessage);
return line;
}
}
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 .
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.
XML
<PropertyGroup>
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>
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.
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
Define the most common return types and status codes returned from a specific
type of action.
Identify actions that deviate from the defined standard.
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.
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();
}
C#
[ProducesDefaultResponseType]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
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
{
Response types
These methods are annotated with [ProducesResponseType] or
[ProducesDefaultResponseType] attributes. For example:
C#
If more specific metadata attributes are absent, applying this convention to an assembly
enforces that:
C#
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Find(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
int id)
{ }
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.
C#
[HttpGet("Throw")]
public IActionResult Throw() =>
throw new Exception("Sample exception.");
Console
Console
<!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;
}
...
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:
C#
app.UseHttpsRedirection();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
}
app.UseAuthorization();
app.MapControllers();
app.Run();
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.
C#
if (app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error-development");
}
else
{
app.UseExceptionHandler("/error");
}
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:
C#
C#
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.
C#
builder.Services.AddControllers(options =>
{
options.Filters.Add<HttpResponseExceptionFilter>();
});
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();
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
C#
builder.Services.AddControllers();
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();
}
// /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 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"
}
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#
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
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();
}
// /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 automatic creation of a ProblemDetails for error status codes is disabled when the
SuppressMapClientErrors property is set to true :
C#
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressMapClientErrors = true;
});
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.
C#
builder.Services.AddControllers();
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = (context) =>
{
.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;
}
}
);
app.UseHttpsRedirection();
app.UseStatusCodePages();
app.UseAuthorization();
app.MapControllers();
app.Run();
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#
A problem details response is generated with the previous code when any of the
following conditions apply:
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."
}
Implement ProblemDetailsFactory
MVC uses Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory to produce
all instances of ProblemDetails and ValidationProblemDetails. This factory is used for:
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
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
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
Console
Usage:
httprepl [<BASE_ADDRESS>] [options]
Arguments:
<BASE_ADDRESS> - The initial base address for the REPL.
Options:
-h|--help - Show help information.
Setup Commands:
Use these commands to configure the tool for your API server
Navigation Commands:
The REPL allows you to navigate your URL space and focus on specific APIs
that you are working on.
Shell Commands:
Use these commands to interact with the REPL shell.
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.
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
For example:
Console
Console
For example:
Console
Console
For example:
Console
Console
https://localhost:5001/> ls
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>
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.
Console
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
Console
When specific color keys aren't set, more generic keys are considered. To demonstrate
this fallback behavior, consider the following 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
JSON
[
{
"id": 1,
"name": "Apple"
},
{
"id": 2,
"name": "Orange"
},
{
"id": 3,
"name": "Strawberry"
}
]
Console
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
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.
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
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
Console
Synopsis
Console
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
-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
Example
To issue an HTTP GET request:
Console
https://localhost:5001/people> get
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>
Console
https://localhost:5001/people> get 2
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>
Synopsis
Console
Arguments
PARAMETER
The route parameter, if any, expected by the associated controller action method.
Options
-F|--no-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
-c|--content
-f|--file
Provides a path to a file containing the HTTP request body. For example, -f
"C:\request.json" .
--no-body
Example
To issue an HTTP POST request:
1. Run the post command on an endpoint that supports it:
Console
JSON
{
"id": 0,
"name": ""
}
Tip
To set the default text editor, see the Set the default text editor section.
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
{
"id": 4,
"name": "Scott Addie"
}
https://localhost:5001/people>
Synopsis
Console
Arguments
PARAMETER
The route parameter, if any, expected by the associated controller action method.
Options
-F|--no-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
-c|--content
-f|--file
Provides a path to a file containing the HTTP request body. For example, -f
"C:\request.json" .
--no-body
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
JSON
{
"id": 0,
"name": ""
}
Tip
To set the default text editor, see the Set the default text editor section.
JSON
{
"id": 2,
"name": "Cherry"
}
4. Save the .tmp file, and close the text editor. The following output appears in the
command shell:
Console
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>
Synopsis
Console
Arguments
PARAMETER
The route parameter, if any, expected by the associated controller action method.
Options
-F|--no-formatting
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
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"
}
]
Console
https://localhost:5001/fruits> delete 2
Console
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>
Synopsis
Console
Arguments
PARAMETER
The route parameter, if any, expected by the associated controller action method.
Options
-F|--no-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
-c|--content
Provides a path to a file containing the HTTP request body. For example, -f
"C:\request.json" .
--no-body
Synopsis
Console
Arguments
PARAMETER
The route parameter, if any, expected by the associated controller action method.
Options
-F|--no-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
Synopsis
Console
Arguments
PARAMETER
The route parameter, if any, expected by the associated controller action method.
Options
-F|--no-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
Console
With the preceding approach, each distinct HTTP request header requires its own
-h option.
Console
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
Console
2. Exit and restart the tool before sending another request to the web API.
Console
2. Exit and restart the tool before sending another request to the web API.
basic authentication
JWT bearer tokens
digest authentication
For example, you can send a bearer token to an endpoint with the following command:
Console
1. Sign in to Azure:
Azure CLI
az login
Azure CLI
Azure CLI
Azure CLI
Console
httprepl https://management.azure.com
Console
Console
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>
]
}
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
{
"id": 0,
"name": "Scott Addie"
}
{
"id": 4,
"name": "Scott Addie"
}
https://localhost:5001/people>
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:
text
2. Execute the run command, passing in the text file's path. For example:
Console
Console
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>
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/>
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.
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.
Data points
The telemetry feature doesn't:
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.
.NET Data
SDK
versions
>=5.0 Hashed Media Access Control (MAC) address: a cryptographically (SHA256) hashed
and unique ID for a machine.
>=5.0 Whether the tool was started with help , run , or connect arguments. Actual argument
values aren't collected.
>=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#
app.Run();
C#
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.
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:
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
Prerequisites
Visual Studio
Visual Studio 2022 with the ASP.NET and web development workload.
Create an API project
Visual Studio
C#
app.Run();
Visual Studio
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.
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.
C#
The preceding code creates the model for this app. A model is a class that represents
data that the app manages.
C#
using Microsoft.EntityFrameworkCore;
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.
C#
using Microsoft.EntityFrameworkCore;
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
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#
The DI container provides access to the database context and other services.
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
C#
Run the app. The browser displays a 404 error because there is no longer a / endpoint.
Select raw.
JSON
{
"name":"walk dog",
"isComplete":true
}
Select Send.
Examine the GET endpoints
The sample app implements several GET endpoints by calling MapGet :
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
C#
Select Send.
JSON
[
{
"id": 1,
"name": "walk dog",
"isComplete": false
}
]
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.
C#
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.
Update the to-do item that has Id = 1 and set its name to "feed fish" :
JSON
{
"id": 1,
"name": "feed fish",
"isComplete": false
}
C#
return Results.NotFound();
});
C#
using Microsoft.EntityFrameworkCore;
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
return Results.NotFound();
});
app.Run();
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;
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
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#
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
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#
Unit test code can verify that an object of type Ok<Todo[]> is returned from the handler
method. For example:
C#
// Act
var result = await TodosApi.GetAllTodos(db);
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.
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#
The secret field needs to be hidden from this app, but an administrative app could
choose to expose it.
C#
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name,
todoItem.IsComplete);
}
C#
using Microsoft.EntityFrameworkCore;
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.Ok(todoItemDTO);
}
return TypedResults.NotFound();
}
Verify you can post and get all fields except the secret field.
Next steps
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:
WebApplication
The following code is generated by an ASP.NET Core template:
C#
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.
C#
app.Run();
conditions:
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
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.UseRouting();
C#
app.UseRouting();
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
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#
app.Run("http://localhost:3000");
Multiple ports
In the following code, the app responds to port 3000 and 4000 .
C#
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.Run();
.NET CLI
configuration
C#
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.
ASPNETCORE_URLS=http://localhost:3000
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#
app.Urls.Add("http://*:3000");
app.Run();
http://+:3000
C#
app.Urls.Add("http://+:3000");
app.Run();
http://0.0.0.0:3000
C#
app.Urls.Add("http://0.0.0.0:3000");
app.Run();
app.Urls.Add("https://localhost:3000");
app.Run();
For more information on the development certificate, see Trust the ASP.NET Core HTTPS
development certificate on Windows and macOS.
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#
app.Urls.Add("https://localhost:3000");
app.Run();
C#
using System.Security.Cryptography.X509Certificates;
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);
});
});
app.Urls.Add("https://localhost:3000");
app.Run();
Configuration
The following code reads from the configuration system:
C#
Logging
The following code writes a message to the log on application startup:
C#
app.Run();
For more information, see Logging in .NET Core and ASP.NET Core
C#
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
app.MapControllers();
app.Run();
WebApplicationBuilder
This section contains sample code using WebApplicationBuilder.
C#
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}");
C#
builder.Configuration.AddIniFile("appsettings.ini");
Read configuration
By default the WebApplicationBuilder reads configuration from multiple sources,
including:
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);
app.Run();
app.Run();
Add services
C#
app.Run();
C#
app.Run();
C#
app.Run();
C#
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
Add Middleware
Any existing ASP.NET Core middleware can be configured on the WebApplication :
C#
app.Run();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an
exception.");
});
app.Run();
Static Files Provides support for serving static files and UseStaticFiles,
directory browsing. UseFileServer
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#
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("/", handler);
app.Run();
Local function
C#
app.MapGet("/", LocalFunction);
app.Run();
Instance method
C#
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
Static method
C#
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
Program.cs
C#
using MinAPISeparateFile;
TodoEndpoints.Map(app);
app.Run();
TodoEndpoints.cs
C#
namespace MinAPISeparateFile;
C#
app.Run();
The preceding code displays The link to the hello endpoint is /hello from the /
endpoint.
Endpoint names:
Route Parameters
Route parameters can be captured as part of the route pattern definition:
C#
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:
C#
app.Run();
Route constraints
Route constraints constrain the matching behavior of a route.
C#
app.Run();
The following table demonstrates the preceding route templates and their behavior:
/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;
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#
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();
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#
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#
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.
.NET CLI
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.
Route values
Query string
Header
Body (as JSON)
Services provided by dependency injection
Custom
The following GET route handler uses some of these parameter binding sources:
C#
// Added as service
builder.Services.AddSingleton<Service>();
class Service { }
The following table shows the relationship between the parameters used in the
preceding example and the associated binding sources.
id route value
customHeader header
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#
C#
// ...
});
// ...
});
C#
using Microsoft.AspNetCore.Mvc;
// Added as service
builder.Services.AddSingleton<Service>();
class Service { }
7 Note
C#
using Microsoft.AspNetCore.Mvc;
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.Run();
URI result
/products? 3 returned
pageNumber=3
To make pageNumber optional, define the type as optional or provide a default value:
C#
app.MapGet("/products2", ListProducts);
app.Run();
URI result
/products?pageNumber=3 3 returned
/products 1 returned
/products2 1 returned
C#
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#
app.Run();
URI result
/products? 3 returned
pageNumber=3
/products 1 returned
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#
C#
C#
C#
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.
C#
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
C#
// 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);
C#
using System.Threading.Channels;
using BackgroundQueueService;
// The max size of a single message, staying below the default LOH size of
85K.
var maxMessageSize = 80 * 1024;
// 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);
app.Run();
C#
app.Run();
There is no built-in support for antiforgery. However, it can be implemented using the
IAntiforgery service.
C#
// 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#
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
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.
C#
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
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.
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());
C#
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
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#
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
return Results.NotFound();
});
The following record types can be used to replace the preceding parameters:
C#
Using a struct with AsParameters can be more performant than using a record type.
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#
The following code displays Point: 12.3, 10.1 with the URI /map?Point=12.3,10.1 :
C#
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
point = null;
return false;
}
}
BindAsync
C#
using System.Reflection;
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy},
" +
$"SortDirection:{pageData.SortDirection}, CurrentPage:
{pageData.CurrentPage}");
app.Run();
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;
return ValueTask.FromResult<PagingData?>(result);
}
}
Binding Precedence
The rules for determining a binding source from a parameter:
C#
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
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.
C#
using System.Text.Json;
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 .
C#
app.Run();
Responses
Route handlers support the following types of return values:
For a more in-depth guide to route handler return values see Create responses in
Minimal API applications
C#
C#
Return TypedResults
The following code returns a TypedResults:
C#
The following example uses the built-in result types to customize the response:
C#
JSON
C#
C#
Text
C#
Stream
C#
Redirect
C#
File
C#
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);
C#
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.
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 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>();
app.UseAuthorization();
app.Run();
C#
C#
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
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>();
app.UseAuthorization();
app.Run();
C#
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#
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
app.Run();
C#
using Microsoft.AspNetCore.Cors;
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
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#
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.
C#
app.Run();
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
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.UseRouting();
C#
app.UseRouting();
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
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#
app.Run("http://localhost:3000");
Multiple ports
In the following code, the app responds to port 3000 and 4000 .
C#
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.Run();
.NET CLI
C#
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.
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
http://*:3000
C#
app.Urls.Add("http://*:3000");
app.Run();
http://+:3000
C#
app.Urls.Add("http://+:3000");
app.Run();
http://0.0.0.0:3000
C#
app.Urls.Add("http://0.0.0.0:3000");
app.Run();
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
app.Urls.Add("https://localhost:3000");
app.Run();
For more information on the development certificate, see Trust the ASP.NET Core HTTPS
development certificate on Windows and macOS.
JSON
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
C#
app.Urls.Add("https://localhost:3000");
app.Run();
C#
using System.Security.Cryptography.X509Certificates;
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);
});
});
app.Urls.Add("https://localhost:3000");
app.Run();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
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#
app.Run();
Logging
The following code writes a message to the log on application startup:
C#
app.Run();
For more information, see Logging in .NET Core and ASP.NET Core
C#
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
app.MapControllers();
app.Run();
WebApplicationBuilder
This section contains sample code using WebApplicationBuilder.
C#
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}");
C#
builder.Configuration.AddIniFile("appsettings.ini");
Read configuration
By default the WebApplicationBuilder reads configuration from multiple sources,
including:
Environment variables
The command line
C#
app.Run();
C#
if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}
app.Run();
app.Run();
Add services
C#
C#
app.Run();
C#
app.Run();
C#
app.Run();
C#
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
Add Middleware
Any existing ASP.NET Core middleware can be configured on the WebApplication :
C#
app.Run();
C#
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an
exception.");
});
app.Run();
Route Handlers in Minimal API apps
Article • 03/30/2023
C#
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#
app.MapGet("/", handler);
app.Run();
Local function
C#
app.MapGet("/", LocalFunction);
app.Run();
Instance method
C#
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
Static method
C#
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
Program.cs
C#
using MinAPISeparateFile;
TodoEndpoints.Map(app);
app.Run();
TodoEndpoints.cs
C#
namespace MinAPISeparateFile;
C#
app.Run();
The preceding code displays The link to the hello endpoint is /hello from the /
endpoint.
Endpoint names:
Route Parameters
Route parameters can be captured as part of the route pattern definition:
C#
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:
C#
app.Run();
Route constraints
Route constraints constrain the matching behavior of a route.
C#
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;
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#
return group;
}
In this scenario, you can use a relative address for the Location header in the 201
Created result:
C#
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#
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#
.NET CLI
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.
Route values
Query string
Header
Body (as JSON)
Services provided by dependency injection
Custom
The following GET route handler uses some of these parameter binding sources:
C#
// Added as service
builder.Services.AddSingleton<Service>();
class Service { }
The following table shows the relationship between the parameters used in the
preceding example and the associated binding sources.
id route value
customHeader header
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#
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#
// ...
});
// ...
});
C#
using Microsoft.AspNetCore.Mvc;
// Added as service
builder.Services.AddSingleton<Service>();
class Service { }
7 Note
C#
using Microsoft.AspNetCore.Mvc;
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#
app.Run();
URI result
/products? 3 returned
pageNumber=3
To make pageNumber optional, define the type as optional or provide a default value:
C#
app.MapGet("/products2", ListProducts);
app.Run();
URI result
/products?pageNumber=3 3 returned
/products 1 returned
/products2 1 returned
C#
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#
app.Run();
URI result
/products? 3 returned
pageNumber=3
URI result
/products 1 returned
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#
C#
C#
C#
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.
C#
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
C#
// 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);
using System.Threading.Channels;
using BackgroundQueueService;
// The max size of a single message, staying below the default LOH size of
85K.
var maxMessageSize = 80 * 1024;
// 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);
app.Run();
C#
app.Run();
There is no built-in support for antiforgery. However, it can be implemented using the
IAntiforgery service.
C#
// 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#
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
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();
});
C#
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
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.
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());
C#
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
await Db.SaveChangesAsync();
return Results.NoContent();
});
return Results.NotFound();
});
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#
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
return Results.NotFound();
});
The following record types can be used to replace the preceding parameters:
C#
Using a struct with AsParameters can be more performant than using a record type.
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.
TryParse
C#
The following code displays Point: 12.3, 10.1 with the URI /map?Point=12.3,10.1 :
C#
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
point = null;
return false;
}
}
BindAsync
BindAsync has the following APIs:
C#
C#
using System.Reflection;
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy},
" +
$"SortDirection:{pageData.SortDirection}, CurrentPage:
{pageData.CurrentPage}");
app.Run();
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;
return ValueTask.FromResult<PagingData?>(result);
}
}
Binding failures
When binding fails, the framework logs a debug message and returns various status
codes to the client depending on the failure mode.
Binding Precedence
The rules for determining a binding source from a parameter:
C#
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.
C#
using System.Text.Json;
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 .
C#
app.Run();
Consider the following route handler, which returns a Hello world text.
C#
The 200 status code is returned with text/plain Content-Type header and the following
content.
text
Hello World
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"}
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.
TypedResults helpers return strongly typed objects, which can improve code
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#
For more information about describing a response type, see OpenAPI support in
minimal APIs.
C#
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#
C#
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#
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#
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#
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#
C#
C#
Text
C#
Stream
C#
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;
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");
});
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);
http.Response.Headers.CacheControl = $"public,max-age=
{TimeSpan.FromHours(24).TotalSeconds}";
Redirect
C#
File
C#
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#
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);
C#
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#
C#
C#
C#
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
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.
C#
using System.Text.Json;
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
// }
C#
using System.Text.Json;
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
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;
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
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();
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>
C#
C#
C#
Alternatively, the OperationId property can be set directly on the OpenAPI annotation.
C#
C#
Alternatively, the list of OpenApiTags can be set on the OpenAPI annotation via the
WithOpenApi extension method.
C#
C#
C#
using Microsoft.AspNetCore.OpenApi;
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.Run();
C#
C#
app
.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
.Produces<IList<Todo>>();
C#
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.
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);
C#
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.
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#
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#
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.
Additional Resources
Authentication and authorization in minimal APIs
Filters in Minimal API apps
Article • 03/17/2023
Minimal API filters allow developers to implement business logic that supports:
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.
C#
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:
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#
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
The following code uses filters that implement the IEndpointFilter interface:
C#
using Filters.EndpointFilters;
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
C#
namespace Filters.EndpointFilters;
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilter(async (efiContext, next) =>
{
var tdparam = efiContext.GetArgument<Todo>(0);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
});
C#
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.
C#
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).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();
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
}
}
C#
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);
});
C#
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
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.
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.
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:
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.
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:
The sample code on GitHub provides an example of unit and integration tests on a
Minimal API app.
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);
Assert.NotNull(notFoundResult);
}
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);
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
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
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.UseRouting();
C#
app.UseRouting();
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
Exceptions
In a Minimal API app, there are two different built-in centralized mechanisms to handle
unhandled exceptions:
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#
app.Map("/exception", ()
=> { throw new InvalidOperationException("Sample Exception"); });
app.Run();
ASP.NET Core apps enable the developer exception page by default when both:
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
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();
C#
app.Run();
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#
app.Run();
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.
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 ):
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.
C#
app.Map("/exception", ()
=> { throw new InvalidOperationException("Sample Exception"); });
app.Run();
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.
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.
C#
Typically, a specific authentication strategy is used. In the following sample, the app is
configured with support for JWT bearer-based authentication.
C#
C#
C#
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
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" 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#
builder.Services.AddAuthentication()
.AddJwtBearer()
.AddJwtBearer("LocalAuthIssuer");
The code creates a new authorization policy, named admin_greetings , that encapsulates
two authorization requirements:
C#
using Microsoft.Identity.Web;
builder.Services.AddAuthorizationBuilder()
.AddPolicy("admin_greetings", policy =>
policy
.RequireRole("admin")
.RequireClaim("scope", "greetings_api"));
app.Run();
When developing on the local machine, the dotnet user-jwts tool can be used to create
bearer tokens.
.NET CLI
7 Note
When invoked on a project, the tool automatically adds the authentication options
matching the generated token to appsettings.json .
.NET CLI
The generated token can then be sent as part of the header in the testing tool of choice.
For example, with curl:
.NET CLI
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.
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.
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.
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
JavaScript client
The JavaScript client runs on the current Node.js long-term support (LTS) release and
the following browsers:
Browser Version
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
This tutorial teaches the basics of building a real-time app using SignalR. You learn how
to:
Prerequisites
Visual Studio
Visual Studio 2022 with the ASP.NET and web development workload.
Create a web app project
Visual Studio
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.
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 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;
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.
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>
In the wwwroot/js folder, create a chat.js file with the following code:
JavaScript
"use strict";
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();
});
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.
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.
.NET CLI
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.
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:
.
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.
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.
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
C#
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.
C#
using Microsoft.AspNetCore.SignalR;
namespace SignalRWebpack.Hubs;
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:
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");
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
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"
},
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
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 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:
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
*,
*::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;
}
JSON
{
"compilerOptions": {
"target": "es5"
}
}
index.ts :
TypeScript
divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});
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
is called.
Console
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.
Visual Studio
Console
This command generates the client-side assets to be served when running the
app. The assets are placed in the wwwroot folder.
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.
Prerequisites
Visual Studio
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.
Visual Studio
7 Note
Visual Studio 2022 or later and .NET Core SDK 6.0.0 or later are required.
Type BlazorServerSignalRApp in the Project name field. Confirm the Location entry
is correct or provide a location for the project. Select Next.
Select Create.
In the Manage NuGet Packages dialog, confirm that the Package source is set to
nuget.org .
If the License Acceptance dialog appears, select I Accept if you agree with the
license terms.
C#
using Microsoft.AspNetCore.SignalR;
namespace BlazorServerSignalRApp.Server.Hubs;
C#
using Microsoft.AspNetCore.ResponseCompression;
using BlazorServerSignalRApp.Server.Hubs;
C#
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
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");
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;
await hubConnection.StartAsync();
}
7 Note
Visual Studio
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:
Next steps
In this tutorial, you learned how to:
To learn more about building Blazor apps, see the Blazor documentation:
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
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.
C#
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
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.
C#
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
=> await Clients.All.SendAsync("ReceiveMessage", user, message);
}
7 Note
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.
Property Description
ConnectionId Gets the unique ID for the connection, assigned by SignalR. There's one
connection ID for each connection.
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
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.
Property Description
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
Method Description
AllExcept Calls a method on all connected clients except for the specified connections
GroupExcept Calls a method on all connections in the specified group, except the specified
connections
OthersInGroup Calls a method on a group of connections, excluding the client that invoked the
hub method
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.
C#
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#
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;
}
}
C#
C#
.NET client
C#
Typescript client
TypeScript
Java client
Java
hubConnection.onWithResult("GetMessage", () -> {
return Single.just("message");
});
C#
[HubMethodName("SendMessageToUser")]
public async Task DirectMessage(string user, string message)
=> await Clients.User(user).SendAsync("ReceiveMessage", user, message);
C#
services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();
// ...
C#
services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();
services.AddSignalR(options =>
{
options.DisableImplicitFromServicesParameters = true;
});
// ...
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.
C#
C#
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
C#
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 .
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.
C#
With access to an instance of IHubContext , call client methods as if you were in the hub
itself:
C#
C#
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:
C#
C#
C#
await next.Invoke();
}
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.
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.
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
C#
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#
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
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
C#
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#
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.
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#
TypeScript
C#
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
The same technique works for methods defined on the client. You can send a custom
object from the server side:
C#
On the client side, you access the Message property rather than using a parameter:
TypeScript
If you later decide to add the sender of the message to the payload, add a property to
the object:
C#
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
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:
This article provides guidance for writing and using hub filters.
C#
C#
hubOptions.AddFilter<TFilter>();
C#
hubOptions.AddFilter(typeof(TFilter));
This will be resolved from DI or type activated.
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.
C#
// 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 .
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#
2. Define a hub filter to check for the attribute and replace banned phrases in a hub
method argument with *** :
C#
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);
}
C#
services.AddSingleton<LanguageFilter>();
}
Hub The instance of the Hub being used for this hub Hub
method invocation.
ServiceProvider The scoped service provider for this hub method IServiceProvider
invocation.
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
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.
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.
Support for enabling additional client features is tracked in our issue tracker .
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.
The code sample in this article is a WPF app that uses the ASP.NET Core SignalR .NET
client.
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();
try
{
await connection.StartAsync();
messagesList.Items.Add("Connection started");
connectButton.IsEnabled = false;
sendButton.IsEnabled = true;
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}
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#
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#
// 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
C#
return Task.CompletedTask;
};
C#
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#
return Task.CompletedTask;
};
C#
// .WithAutomaticReconnect(new[] { TimeSpan.Zero,
TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)
}) yields the default behavior.
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
C#
C#
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#
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#
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) .
C#
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.
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.
Interfaces
IHubConnectionBuilder A builder abstraction for configuring HubConnection instances.
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
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.
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
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) .
Java
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:
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.
Java
TypeReference class. For example, using a custom generic class named Foo<T> , the
following code gets the Type :
Java
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
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
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
Classes
CancelInvocationMessage
CloseMessage
CompletionMessage
InvocationBindingFailureMessage
InvocationMessage
PingMessage
StreamBindingFailureMessage
StreamInvocationMessage
StreamItem
TypeReference<T> A utility for getting a Java Type from a literal generic Class.
UserAgentHelper
Interfaces
Action A callback that takes no parameters.
Enums
HubConnectionState Indicates the state of the HubConnection.
HubMessageType
Feedback
Was this page helpful? ツ Yes ト No
By Rachel Appel
The ASP.NET Core SignalR JavaScript client library enables developers to call server-side
SignalR hub code.
Visual Studio
Bash
npm init -y
npm install @microsoft/signalr
Reference the SignalR JavaScript client in the <script> element. For example:
HTML
<script src="~/lib/signalr/signalr.js"></script>
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-
signalr/6.0.1/signalr.js"></script>
cdnjs
jsDelivr
unpkg
Connect to a hub
The following code creates and starts a connection. The hub's name is case insensitive:
JavaScript
connection.onclose(async () => {
await start();
});
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;
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
builder.WithOrigins("https://example.com")
.AllowAnyHeader()
.WithMethods("GET", "POST")
.AllowCredentials();
});
});
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");
app.Run();
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