ASP.NET Core Guide for Developers
ASP.NET Core Guide for Developers
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 });